什么是ThreadLocal?
ThreadLocal 是 Java 提供的一个线程本地变量工具类,用于在多线程环境下为每个线程提供独立的变量副本。简单来说,它能让每个线程拥有自己的"储物柜",存储仅对自己可见的数据,避免线程间的数据竞争,同时减少同步锁的开销。
当你创建一个 ThreadLocal
变量时,每个访问该变量的线程都会拥有一个独立的副本。这也是 ThreadLocal
名称的由来。线程可以通过 get()
方法获取自己线程的本地副本,或通过 set()
方法修改该副本的值,从而避免了线程安全问题。
ThreadLocal的原理了解吗?
简单看一下ThreadLocal的源码
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
从上面Thread
类 源代码可以看出Thread
类中有一个 threadLocals
和 一个inheritableThreadLocals
变量,它们都是 ThreadLocalMap
类型的变量,我们可以把 ThreadLocalMap
理解为ThreadLocal
类实现的定制化的 HashMap
。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal
类的 set
或get
方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap
类对应的 get()
、set()
方法。
看一下ThreadLocal的set方法:
public void set(T value) {
//获取当前请求的线程
Thread t = Thread.currentThread();
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
ThreadLocalMap map = getMap(t);
if (map != null)
// 将需要存储的值放入到这个哈希表中
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
由此可以看出,最终的变量是存储在当前线程的ThreadLocalMap上,而并非ThreadLocal上,ThreadLocal可以理解为ThreadLocalMap的封装,传递了变量值。ThreadLocal类可以通过Thread.currentThread()方法获取当前线程对象后,直接通过getMap(Thread)方法可以访问到该线程的ThreadLocalMap对象。
每个Thread中都存在一个ThreadLocalMap对象,而ThreadLocalMap可以存储以ThreadLocal对象为key,以Object为value的键值对。
加入我们在同一个线程中声明了两个ThreadLocal对象,都是仅由Thread中唯一的ThreadLocalMap来存储,以ThreadLocal对象为key,value就是ThreadLocal对象调用的set方法设置的值。
ThreadLocal内存泄漏是怎么导致的?
ThreadLocal
内存泄漏的根本原因在于其内部实现机制。
通过上面,我们已经直到,当使用ThreadLocal存储值时,实际上是存储到了ThreadLocalMap中,以ThreadLocal对象为key,以set方法设置的值为value。
要想学习ThreadLocal的内存泄露问题,我们首先要对ThreadLocalMap的引用机制做个了解。
ThreadLocalMap中key和value的引用机制:
key是弱引用:
ThreadLocalMap
中的 key 是ThreadLocal
的弱引用 。 这意味着,如果ThreadLocal
实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致ThreadLocalMap
中对应的 key 变为null
。value是强引用: 即使
key
被 GC 回收,value
仍然被ThreadLocalMap.Entry
强引用存在,无法被 GC 回收。
打个比方:
想象你有一个 "共享储物柜"(ThreadLocal),每个线程(比如员工)可以往储物柜里存自己的私人物品(value)。但储物柜有个特殊规则:
钥匙(Key)是纸做的(弱引用):如果员工把钥匙弄丢了(ThreadLocal 实例被回收),储物柜管理员(GC)会主动清理钥匙。
储物柜里的物品(Value)是铁盒装的 (强引用):即使钥匙丢了,铁盒还在储物柜里,除非员工自己主动清理(调用
remove()
)。问题来了:如果储物柜一直存在(比如线程池中的线程长期存活),而员工频繁更换钥匙但不清除旧铁盒,储物柜里会堆满无法打开的废铁盒(内存泄漏)。
当 ThreadLocal
实例失去强引用后,其对应的 value 仍然存在于 ThreadLocalMap
中,因为 Entry
对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap
也会一直存在,导致 key 为 null
的 entry 无法被垃圾回收,即会造成内存泄漏。
也就是说,内存泄漏的发生需要同时满足两个条件:
ThreadLocal
实例不再被强引用;- 线程持续存活,导致
ThreadLocalMap
长期存在。
如何避免ThreadLocal的内存泄露呢?
- 在使用完
ThreadLocal
后,务必调用remove()
方法。 这是最安全和最推荐的做法。remove()
方法会从ThreadLocalMap
中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将ThreadLocal
定义为static final
,也强烈建议在每次使用后调用remove()
。 - 在线程池等线程复用的场景下,使用
try-finally
块可以确保即使发生异常,remove()
方法也一定会被执行。
如何跨线程传递ThreadLocal的值呢?
在 Java 中,ThreadLocal
的设计初衷是为每个线程提供独立的变量副本,天然不支持跨线程传递值。但实际开发中(如异步任务、线程池场景),常需要将主线程的上下文传递给子线程。
如果想要在异步场景下传递 ThreadLocal
值,有两种解决方案:
InheritableThreadLocal
:InheritableThreadLocal
是 JDK1.2 提供的工具,继承自ThreadLocal
。使用InheritableThreadLocal
时,会在创建子线程时,令子线程继承父线程中的ThreadLocal
值,但是无法支持线程池场景下的ThreadLocal
值传递。TransmittableThreadLocal
:TransmittableThreadLocal
(简称 TTL) 是阿里巴巴开源的工具类,继承并加强了InheritableThreadLocal
类,可以在线程池的场景下支持ThreadLocal
值传递。
InheritableThreadLocal
原理
InheritableThreadLocal
实现了创建异步线程时,继承父线程 ThreadLocal
值的功能。该类是 JDK 团队提供的,通过改造 JDK 源码包中的 Thread
类来实现创建线程时,ThreadLocal
值的传递。
InheritableThreadLocal
的值存储在哪里?
在 Thread
类中添加了一个新的 ThreadLocalMap
,命名为 inheritableThreadLocals
,该变量用于存储需要跨线程传递的 ThreadLocal
值。如下:
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
如何完成 ThreadLocal
值的传递?
通过改造 Thread
类的构造方法来实现,在创建 Thread
线程时,拿到父线程的 inheritableThreadLocals
变量赋值给子线程即可。相关代码如下
// Thread 的构造方法会调用 init() 方法
private void init(/* ... */) {
// 1、获取父线程
Thread parent = currentThread();
// 2、将父线程的 inheritableThreadLocals 赋值给子线程
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
TransmittableThreadLocal原理
JDK 默认没有支持线程池场景下 ThreadLocal
值传递的功能,因此阿里巴巴开源了一套工具 TransmittableThreadLocal
来实现该功能。
阿里巴巴无法改动 JDK 的源码,因此他内部通过 装饰器模式 在原有的功能上做增强,以此来实现线程池场景下的 ThreadLocal
值传递。
TTL 改造的地方有两处:
-
实现自定义的
Thread
,在run()
方法内部做ThreadLocal
变量的赋值操作。 -
基于 线程池 进行装饰,在
execute()
方法中,不提交 JDK 内部的Thread
,而是提交自定义的Thread
。
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!