ThreadLocal的使用以及原理

ThreadLocal的使用以及原理

   概要

   ThreadLocal 是 java 提供的一个方便对象在本线程内不同方法中进行传递和获取的类。用它定义的变量,仅在本线程中可见和维护,不受其他线程的影响,与其他线程相互隔离。

   一、ThreadLocal能解决什么问题?

   当涉及一个对象需要在很多不同方法之间传递时,应该考虑使用 ThreadLocal 对象来简化代码。

   1.  线程专属的本地变量

   通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?JDK 中自带的ThreadLocal类正是为了解决这样的问题。

   2. 方法参数传递

   虽然在本线程不同方法中使用变量,可以通过在方法中传入参数解决,但是当涉及多个方法甚至多个类时,为每个方法增加同样的参数将是一场噩梦,此时 ThreadLocal 就能很好地解决这个问题。它可以在本线程内任何一个地方赋值,在任何一个地方获取值,并且不用作为函数参数传入。

   3. ThreadLocal 线程隔离

   上面第2点提到的方法参数传递的问题,静态成员变量也能解决。但是ThreadLocal 变量相比静态成员变量的一个优势就是,ThreadLocal 是线程隔离的,其值不会受另一个线程的影响,也不用考虑加锁或值被其他线程篡改的问题,而这些问题都是静态成员变量无法做到的。

   二、ThreadLocal原理

   1.  Thread 

   从Thread 类源码入手

1 public class Thread implements Runnable {
2     //......
3     //与此线程有关的ThreadLocal值。由ThreadLocal类维护
4     ThreadLocal.ThreadLocalMap threadLocals = null;
5 
6     //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
7     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
8     //......
9 }

   可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。

   ThreadLocal 能在每个线程间进行隔离,其主要是靠在每个 Thread 对象中维护一个 ThreadLocalMap 来实现的。因为是线程中的对象,所以对其他线程不可见,从而达到隔离的目的。之所以是Map结构,主要是因为一个线程中可能有多个 ThreadLocal 对象,这就需要一个集合来进行存储区分,而用 Map 可以更快地查找到相关的对象。

   1. ThreadLocalMap

   ThreadLocalMap 是 ThreadLocal 对象的一个静态内部类,内部维护一个 Entry 数组,实现类似 Map 的 get 和 put 等操作,为简单起见,可以将其看做是一个 Map,其中 key 是 ThreadLocal 实例,value 是 ThreadLocal 实例对象存储的值。部分源码如下:

 1 public class ThreadLocal<T> {
 2     private final int threadLocalHashCode = nextHashCode();
 3     private static AtomicInteger nextHashCode = new AtomicInteger();
 4     //...
 5         
 6     static class ThreadLocalMap {
 7         // 内部 Entry 类,用于存储 ThreadLocal 和关联的值
 8         static class Entry extends WeakReference<ThreadLocal<?>> {
 9             // 与当前 ThreadLocal 关联的具体值
10             Object value;
11 
12             // 构造方法,接受 ThreadLocal 键和对应的值
13             Entry(ThreadLocal<?> k, Object v) {
14                 super(k);  // 将 ThreadLocal 键作为弱引用存储
15                 value = v;
16             }
17         }
18     }
19 }

   说明:ThreadLocal 键是一个弱引用。如果 ThreadLocal 实例在其他地方没有强引用,GC 会回收该 ThreadLocal 对象,避免内存泄漏。这在长时间运行的多线程应用中非常重要,尤其是防止因 ThreadLocal 没有被及时清理而导致的内存泄漏问题。 

   这里要注意一个问题:ThreadLocal为什么是WeakReference呢?

   原因如下:

   1)如果是强引用的话,即使ThreadLocal的值是为null,但是的话ThreadLocalMap还是会有ThreadLocal的强引用状态,如果没有手动进行删除的话,ThreadLocal就不会被回收,这样就会导致Entry内存的泄漏。
   2)如果是弱引用的话,引用ThreadLocal的对象被回收掉了,ThreadLocalMap还保留有ThreadLocal的弱引用,即使没有进行手动删除,ThreadLocal也会被回收掉。value在下一次的ThreadLocalMap调用set/get/remove方法的时候就会被清除掉。

   2. set

   当调用 ThreadLocal 的 set 方法给变量设置值时,ThreadLocal 对象会先获取本线程的 ThreadLocalMap 对象,然后将当前的 ThreadLocal 对象及要设置值作为键值对放入 Map 中。

   部分源码如下:

 1 public void set(T value) {
 2 // 获取当前请求的线程
 3     Thread t = Thread.currentThread();
 4 
 5     // 取出Thread 类内部的 threadLocals 变量(哈希表结构)
 6     ThreadLocalMap map = getMap(t);
 7     if (map != null)
 8         // this 指当前的 ThreadLocal 对象,将需要存储的值放入到这个哈希表中
 9         map.set(this, value);
10     else
11         // key 不存在,则创建 map 并设置值
12         createMap(t, value);
13 }
14 
15 ThreadLocalMap getMap(Thread t) {
16     // threadLocals 是 Thread 中的一个变量,因此是线程隔离的,不会受其他线程影响
17     // 其在 Thread 类中的定义如下:ThreadLocal.ThreadLocalMap threadLocals = null;
18     return t.threadLocals;
19 }

    每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

    ThreadLocalMap#set方法:

 1 private void set(ThreadLocal<?> key, Object value) {
 2     // 不像 get() 方法那样有快速路径,因为 set() 既可能用于创建新条目
 3     // 也可能用于替换现有条目,快速路径失败的概率较高。
 4 
 5     Entry[] tab = table;  // 获取当前 ThreadLocalMap 中的条目数组
 6     int len = tab.length;  // 数组长度
 7     int i = key.threadLocalHashCode & (len - 1);  // 计算索引位置
 8 
 9     // 遍历当前索引位置及后续位置,查找匹配的 Entry
10     for (Entry e = tab[i];
11          e != null;  // 如果该位置的 Entry 不为空
12          e = tab[i = nextIndex(i, len)]) {  // 更新索引到下一个位置
13         ThreadLocal<?> k = e.get();  // 获取当前 Entry 中的 ThreadLocal
14 
15         if (k == key) {
16             // 如果找到匹配的 ThreadLocal,更新其值
17             e.value = value;
18             return;
19         }
20 
21         if (k == null) {
22             // 如果找到的 ThreadLocal 已被回收,替换为新的 Entry
23             replaceStaleEntry(key, value, i);
24             return;
25         }
26     }
27 
28     // 如果没有找到匹配的 Entry,则在当前索引位置创建新的 Entry
29     tab[i] = new Entry(key, value);
30     int sz = ++size;  // 更新当前 ThreadLocalMap 的大小
31     // 清理槽位,或者在达到阈值时进行重新哈希
32     if (!cleanSomeSlots(i, sz) && sz >= threshold)
33         rehash();
34 }

    在 for 循环中,通过更新索引 i = nextIndex(i, len) 来解决哈希冲突。使用开放定址法(线性探测的方式)来有效处理了哈希冲突,使得 ThreadLocal 的值能够正确存取。

    3.  get

    获取 ThreadLocal 存储的对象值时,需要调用 get 方法。此方法也是先获取本线程的 ThreadLocalMap 对象,然后将当前的 ThreadLocal 对象作为 key 从 Map 中获取对应的值,如果没有,则返回一个初始 null。

 1 public T get() {
 2     // 获取当前执行该方法的线程实例
 3     Thread t = Thread.currentThread();
 4     // 获取该线程的 ThreadLocalMap 对象,用于存储 ThreadLocal 变量的值
 5     ThreadLocalMap map = getMap(t);
 6     
 7     // 检查当前线程是否已拥有一个 ThreadLocalMap
 8     if (map != null) {
 9         // 在 ThreadLocalMap 中查找当前 ThreadLocal 实例 (this) 对应的 Entry
10         // this 指当前的 ThreadLocal 对象
11         ThreadLocalMap.Entry e = map.getEntry(this);
12         
13         // 如果找到与当前 ThreadLocal 匹配的 Entry,返回其存储的值
14         if (e != null) {
15             // 忽略类型转换的警告,将泛型类型强制转换为 T
16             @SuppressWarnings("unchecked")
17             T result = (T)e.value;
18             return result;
19         }
20     }
21     
22     // 初始化该 ThreadLocal 的值
23     return setInitialValue();
24 }

    ThreadLocal#getEntry()

 1 private Entry getEntry(ThreadLocal<?> key) {
 2     // 计算 key 的哈希值在 table 中的索引位置
 3     int i = key.threadLocalHashCode & (table.length - 1);
 4     
 5     // 从 table 数组中获取该索引位置的 Entry
 6     Entry e = table[i];
 7     
 8     // 如果该索引位置的 Entry 不为空,且 Entry 中的键与当前 key 匹配
 9     if (e != null && e.get() == key)
10         // 返回该 Entry
11         return e;
12     else
13         // 否则,调用 getEntryAfterMiss 方法进行进一步查找(即发生哈希冲突时)
14         return getEntryAfterMiss(key, i, e);
15 }

    getEntry 方法实现了对 ThreadLocal 对象存储值的快速查找。当出现哈希冲突时,通过线性探测继续查找后续槽位,确保最终可以找到匹配的 Entry 或插入一个新值。这个地方解决哈希冲突使用了开放定址法,保证了ThreadLocal的查找、插入过程不需要借助额外的数据结构,适合少量哈希冲突的情况。

    三、ThreadLocal的使用场景

    典型场景1: 在登录成功后通过 ThreadLocal 存储用户信息是一个常见的应用场景,尤其在 Web 应用中处理用户请求时,可以通过 ThreadLocal 将用户信息存储到当前线程上下文中。

    实现步骤如下:

    1.  创建 ThreadLocal 存储用户信息

    定义一个 ThreadLocal 变量,用于存储用户信息(如用户 ID、用户名、角色等)。

 1 public class UserHolder {
 2     public static final int DEFAULT_USER_ID = 0;
 3     private static ThreadLocal<UserContext> threadLocal = new ThreadLocal<>();
 4 
 5     public static UserContext get() {
 6         return threadLocal.get();
 7     }
 8 
 9     public static void set(UserContext context) {
10         threadLocal.set(context);
11     }
12 
13     public static void clear() {
14         threadLocal.remove();
15     }
16 }

   2.  登录成功后存储用户信息

   在登录认证成功后,将用户信息存储到 ThreadLocal 中。

1 public class AuthService {
2     public void login(String username, String password) {
3         // 登录校验逻辑
4         UserInfo user = authenticate(username, password);
5 
6         // 登录成功后存储用户信息到 ThreadLocal
7         UserHolder.set(user);
8     }
9 }

   3.  在后续方法中获取用户信息

   后续处理过程中可以直接调用 UserHolder.get() 获取当前用户的信息。

1 public class OrderService {
2     public void createOrder() {
3         UserInfo currentUser = UserHolder.get();
4         if (currentUser != null) {
5             // 使用当前用户信息创建订单
6         }
7     }
8 }

    4.  清理资源

    在请求处理结束时调用 clear 方法清理 ThreadLocal中的数据,避免内存泄漏。 这里介绍项目中通过拦截器的方式,在完成请求处理后自动清理资源

    1)实现 HandlerInterceptor 接口

    创建一个拦截器类实现 HandlerInterceptor,并在 afterCompletion 中清理 ThreadLocal。

 1 public class UserContextInterceptor implements HandlerInterceptor {
 2 
 3     @Override
 4     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 5         // 这里可以放置用户验证和设置逻辑
 6         return true;
 7     }
 8 
 9     @Override
10     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
11             throws Exception {
12         // 请求完成后清理 ThreadLocal
13         UserHolder.clear();
14     }
15 }

   将该拦截器添加到 Spring MVC 的拦截器链中,这样在每个请求处理完成后, afterCompletion 就会自动执行。

1 @Configuration
2 public class WebConfig implements WebMvcConfigurer {
3 
4     @Override
5     public void addInterceptors(InterceptorRegistry registry) {
6         registry.addInterceptor(new UserContextInterceptor()).addPathPatterns("/**");
7     }
8 }

    3)自动调用

    一旦拦截器注册到 Spring MVC 的拦截器链,afterCompletion 将在每次请求处理结束时自动调用,无需手动调用它。

    四、内存泄露

    1.  ThreadLocal内存泄露是如何导致的?

    ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。如下图:

    说明:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

    2. 如何避免内存泄露?

    ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。

    为了保险起见,建议在使用完 ThreadLocal 对象后手动调用一下 remove 方法清理一下值。

    五、线程池中使用ThreadLocal的注意事项     

    线程池中的线程是复用的,而 ThreadLocal 的数据绑定到线程本地存储,容易导致以下问题:

    1. 数据残留

    未清理的旧任务数据会影响后续任务。

    2. 内存泄漏

    线程池线程长时间存活,而 ThreadLocal 的引用无法释放,可能导致内存泄漏,尤其是在使用大对象作为 ThreadLocal 的值时。

    3. 总结

    除非有明确的需求和良好的清理机制,通常不会在线程池中随意使用 ThreadLocal。

  

    参考链接:

    https://juejin.cn/post/6844904016288317448

posted @ 2024-11-01 17:31  欢乐豆123  阅读(381)  评论(0编辑  收藏  举报