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