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 方法设置初始值并返回

posted @   诸葛匹夫  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示