ThreadLocal详解
功能
ThreadLocal功能非常强大,主要用来存储不同线程中的数据,多个线程向ThreadLocal中设置
值的时候不会发生并发安全问题,多个线程从ThreadLocal中取值的时候也不会发生数据错乱问题。所以在多线程编程领域,它有着强大的作用。
应用
1.spring的声明式事务
在spring框架中,spring的声明式事务给我们解决事务带来了极大的方便。那么从jdbc角度
分析执行多个sql之所以会产生事务问题,是因为没有使用同一个连接,这里的原因有很多,有种情况就是多个线程获取到的Connection对象不是同一个,或者说某个线程拿到了别人的连接对象进行了事务的提交或者回滚,这样就会发生事务的问题。为了解决这个问题,spring内部使用了ThreadLocal对象来存储Connection,这样就保证了不同的线程获取到的是同一个Connection,而且是自己设置的Connection,从而解决事务问题。
2.对登录用户信息的保存
在web开发中,通常要获取当前登录用户的一些信息,比如用户id、昵称等信息。而且经常在
很多接口中需要这些信息。所以我们可以利用springmvc提供的拦截器来实现这个功能。
具体代码如下:
1 @Component 2 public class LoginInterceptor implements HandlerInterceptor { 3 4 //public Long userId; 5 private static final ThreadLocal<UserInfo> THREAD_LOCAL = new ThreadLocal<>(); 6 7 /** 8 * 获取请求头信息中的userId,传递给后续操作 9 * @param request 10 * @param response 11 * @param handler 12 * @return 13 * @throws Exception 14 */ 15 @Override 16 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 17 18 String userId = request.getHeader("userId"); 19 if (StringUtils.isNotBlank(userId)){ 20 UserInfo userInfo = new UserInfo(); 21 userInfo.setUserId(Long.valueOf(userId)); 22 // 通过thread_local传递给后续业务逻辑 23 THREAD_LOCAL.set(userInfo); 24 } 25 26 return true; 27 } 28 29 public static UserInfo getUserInfo(){ 30 return THREAD_LOCAL.get(); 31 } 32 33 @Override 34 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 35 // 由于使用的是tomcat线程池,所以请求结束,线程没有结束。否则就会引发内存泄漏 36 THREAD_LOCAL.remove(); 37 } 38 }
这里一定要注意,ThreadLocal操作不当可能引发内存泄漏,所以一定要在用完ThreadLocal中的对象后,要移除掉。这里在afterCompletion方法中移除对象。
原理
ThreadLocal内部维护了一个ThreadLocalMap对象,这个map对象是一个标准的map实现,内部有一个元素类型为Entry的数组,用以存放线程可能需要的多个副本变量。
关键代码
1 static class ThreadLocalMap { 2 static class Entry extends WeakReference<ThreadLocal<?>> { 3 //这个value就是调用set方法的参数 4 Object value; 5 6 /*类似于map的k,v结构,k就是ThreadLocal对象,value就是需要隔离访问的对象*/ 7 Entry(ThreadLocal<?> k, Object v) { 8 super(k); 9 value = v; 10 } 11 } 12 //用数组保存了Entry,因为可能有多个变量需要线程隔离访问 13 private Entry[] table;
关键的set方法是怎么执行的
1 public void set(T value) { 2 //获取调用这个方法的线程,也就是当前线程 3 Thread t = Thread.currentThread(); 4 //根据当前线程去获取ThreadLocalMap 对象,很显然第一次还没有存 5 ThreadLocalMap map = getMap(t); 6 if (map != null)
7 map.set(this, value); 8 else 9 //创建ThreadLocalMap并且保存value 10 createMap(t, value); 11 }
在CreateMap中创建了ThreadLocalMap,并且通过构造器保存了value
1 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { 2 table = new Entry[INITIAL_CAPACITY]; 3 //通过hash算法计算Entry所在数组的哪个位置 4 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 5 //保存在数组中 6 table[i] = new Entry(firstKey, firstValue); 7 size = 1; 8 setThreshold(INITIAL_CAPACITY); 9 }
set(T value)方法小结
ThreadLocal
的 set
方法的核心逻辑是:首先获取当前线程,然后获取该线程的 ThreadLocalMap
,如果 ThreadLocalMap
已经存在,则将当前 ThreadLocal
对象和传入的值存入 ThreadLocalMap
中;如果 ThreadLocalMap
不存在,则创建一个新的 ThreadLocalMap
并将其关联到当前线程,同时将当前 ThreadLocal
对象和传入的值作为初始键值对存入 ThreadLocalMap
中。通过这种方式,每个线程都可以拥有自己独立的 ThreadLocal
变量副本
再来看看ThreadLocalMap的set方法,这个方法比较复杂
1 private void set(ThreadLocal<?> key, Object value) { 2 3 // 获取当前 ThreadLocalMap 的内部存储数组 4 Entry[] tab = table; 5 // 获取数组的长度 6 int len = tab.length; 7 8 // 通过 ThreadLocal 的哈希码和数组长度进行位运算,计算出键应该存储的初始索引位置 9 // threadLocalHashCode 是 ThreadLocal 类的一个属性,用于计算哈希值 10 // 使用位运算(与操作)确保索引值在数组长度范围内 11 int i = key.threadLocalHashCode & (len - 1); 12 13 // 从计算出的初始索引位置开始遍历数组,处理哈希冲突 14 // 这里使用开放寻址法来解决哈希冲突,即如果当前位置已经被占用,则尝试下一个位置 15 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { 16 // 获取当前条目的键 17 ThreadLocal<?> k = e.get(); 18 19 // 如果当前条目的键与要存储的键相同,说明键已经存在于映射中 20 if (k == key) { 21 // 更新该键对应的值 22 e.value = value; 23 // 更新完成后直接返回 24 return; 25 } 26 27 // 如果当前条目的键为 null,说明该条目是一个过期条目(弱引用被垃圾回收) 28 if (k == null) { 29 // 调用 replaceStaleEntry 方法替换这个过期条目 30 // 该方法会清理一些过期条目,并将新的键值对存储在合适的位置 31 replaceStaleEntry(key, value, i); 32 // 处理完成后直接返回 33 return; 34 } 35 } 36 37 // 如果遍历过程中没有找到相同的键,也没有遇到过期条目 38 // 则在当前索引位置创建一个新的条目 39 tab[i] = new Entry(key, value); 40 // 增加映射中条目的数量 41 int sz = ++size; 42 43 // 调用 cleanSomeSlots 方法尝试清理一些过期条目 44 // 如果清理后没有清理到任何过期条目,并且当前条目数量达到或超过阈值 45 if (!cleanSomeSlots(i, sz) && sz >= threshold) 46 // 调用 rehash 方法进行扩容和重新哈希操作 47 rehash(); 48 } 49 //获取下一个索引位置,当索引到达数组末尾时,会循环回到数组开头、 50 private static int nextIndex(int i, int len) { 51 return ((i + 1 < len) ? i + 1 : 0); 52 }
set(ThreadLocal<?> key, Object value) 方法小结
1.计算键的初始存储索引位置
2.从初始索引位置开始遍历数组,处理哈希冲突
3.如果找到相同的键,则更新其对应的值
4.如果遇到过期条目,则调用 replaceStaleEntry 方法进行处理
5.如果遍历过程中没有找到相同的键,也没有遇到过期条目,则在当前索引位置创建一个新的条目
6.增加条目数量,并尝试清理一些过期条目
7.如果清理后没有清理到任何过期条目,并且当前条目数量达到或超过阈值,则进行扩容和重新哈希操作
ThreadLocal的get()方法原理
1 public T get() { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 ThreadLocalMap.Entry e = map.getEntry(this); 6 if (e != null) { 7 @SuppressWarnings("unchecked") 8 T result = (T)e.value; 9 return result; 10 } 11 }
//这个方法就是返回null,大家可以去看看,不难 12 return setInitialValue(); 13 }
get方法源码相对简单
1.获取当前执行的线程。
2.尝试从当前线程中获取对应的 ThreadLocalMap。
3.如果 ThreadLocalMap 存在,则尝试从中查找与当前 ThreadLocal 对象关联的 Entry。
4.如果找到对应的 Entry,则返回其存储的值。
5.如果 ThreadLocalMap 不存在或者没有找到对应的 Entry,则调用 setInitialValue 方法设置初始值并返回
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?