ThreadLocal底层原理解析
ThreadLocal
ThreadLocal
是Java
中提供的一种 线程局部变量机制。它可以让每个线程都拥有自己独立的变量副本,互不干扰
底层原理
核心机制:每个线程中都有一个 ThreadLocalMap
,它会将 ThreadLocal
对象作为键,对应的值作为值进行存储。
结构为:Thread → ThreadLocalMap → Entry → ThreadLocal
Thread
在Thread
类有一个ThreadLocalMap
,意味着每个线程拥有一个ThreadLocalMap
实例
/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap
ThreadLocalMap
类是在ThreadLocal
类中定义的,它是ThreadLocal
类的静态内部类
ThreadLocalMap
不需要依赖 ThreadLocal 实例,所以它是静态内部类ThreadLocalMap
不是一个通用的 Map,它专为ThreadLocal
服务,甚至用ThreadLocal
实例作为 key。所以它以内部类的形式封装在ThreadLocal
类中
public class ThreadLocal<T> {static class ThreadLocalMap {// 自定义 Map 实现,用来存储每个线程的局部变量}
}
Entry
ThreadLocalMap
类中又定义了一个特殊的内部类 Entry
,它是ThreadLocalMap
中的元素,每个Entry
表示一个 ThreadLocal
对象及其值的绑定。
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}private Entry[] table;
}
这个 Entry
类不是普通的 Map.Entry
,它是一个专门用于 ThreadLocalMap
的键值对结构
字段 | 含义 |
---|---|
ThreadLocal<?> k (继承自 WeakReference ) | 键:ThreadLocal 实例,用弱引用包裹 |
Object value | 值:ThreadLocal 实例对应的值 |
super(k)
会调用父类WeakReference<ThreadLocal<?>>
的构造方法将ThreadLocal
实例包装成弱引用
为什么 Entry 继承 WeakReference?
一句话:为了解决内存泄漏
如果 ThreadLocalMap.Entry
用的是强引用来保存 key(ThreadLocal
对象),那么一旦用户不再使用这个 ThreadLocal
,Map 仍然持有这个引用,导致无法被 GC 回收。使用 弱引用(WeakReference<ThreadLocal<?>>
),当外部不再持有 ThreadLocal
对象时,GC 可以正常清理它。
举例理解
ThreadLocal<String> t = new ThreadLocal<>();
t.set("hello");
t = null; // 用户代码放弃了引用
Entry
拥有的是ThreadLocal
实例的弱引用,t
拥有的是ThreadLocal
实例的强引用;由于此时ThreadLocal
实例有强引用不会被回收,t = null
之后,t
不再拥有ThreadLocal
实例的强引用;当GC
来临时就会被回收。
内存泄漏
值(value
)是强引用,不会自动回收
如果 key == null
,说明 ThreadLocal
被回收了,但 value
还在,可能导致内存泄漏。
防范措施:
-
使用
ThreadLocal
后务必调用remove()
-
ThreadLocalMap
实现中有一个重要的清理机制(expungeStaleEntry
),用于清理键为null
(被 GC 回收)的stale entry
,防止内存泄漏。它不是显示调用的,调用时机如下:ThreadLocal.set()└─► ThreadLocalMap.set()└─► 若发现 key == null└─► expungeStaleEntry(...)ThreadLocal.get()└─► ThreadLocalMap.getEntry()└─► 若 entry.get() == null└─► expungeStaleEntry(...)ThreadLocal.remove()└─► ThreadLocalMap.remove()└─► expungeStaleEntry(...)
既然Value会导致内存泄漏,那为什么不把 value 也用弱引用包装?
因为 value
是由业务方控制生命周期的,不能随便被 GC
自动回收,否则会导致程序出错或行为异常
举例说明(假设value
是弱引用):
ThreadLocal<String> t = new ThreadLocal<>();
tl.set(new String("hello")); // new 的,没有其他强引用System.gc();
String s = t.get(); // 可能是 null,因为 hello 被 GC 掉了
t
还没有使用,里面的值就已经被回收掉了,导致t.get()
返回的是null
总结:ThreadLocalMap
只对 key
使用弱引用,是为了避免泄漏;value
保留强引用,是为了让程序数据可控,防止 GC
随意清除业务数据。
ThreadLocal
的 set()
、get()
、remove()
是如何工作的?
都是先获取当前线程:Thread t = Thread.currentThread()
再获取当前线程的ThreadLocalMap
:ThreadLocalMap map = getMap(t)
然后执行各自的逻辑
如何让子线程继承父线程的 ThreadLocal 变量?
想让子线程继承,可以用:InheritableThreadLocal
,它是ThreadLocal
的子类
在Thread
类中有inheritableThreadLocals
属性
/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
当创建子线程时,调用Thread
类的构造函数,在构造函数中,会检查当前线程(父线程)的 inheritableThreadLocals
变量:
if (parent.inheritableThreadLocals != null) {this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
如果父线程有 inheritableThreadLocals
,则调用 createInheritedMap()
复制一份给子线程。
关键注意事项
-
复制是浅拷贝(引用传递),如果值是可变对象(如
HashMap
),父子线程可能修改同一对象。 -
线程池中的线程是复用的,子线程创建时只会复制一次父线程的变量。后续任务复用该线程时,不会重新复制。
ExecutorService executor = Executors.newFixedThreadPool(1); inheritableThreadLocal.set("任务1的值"); executor.submit(() -> System.out.println("任务1: " + inheritableThreadLocal.get())); // 输出 "任务1的值"inheritableThreadLocal.set("任务2的值"); executor.submit(() -> System.out.println("任务2: " + inheritableThreadLocal.get())); // 仍输出 "任务1的值"!
-
子线程创建时需要复制父线程的变量,可能影响性能(尤其在大量短生命周期线程场景)。
应用场景
- 用户会话信息的存储:Web 应用中,每个请求由不同线程处理,需将用户身份(如 ID、Token)与线程绑定
- 数据库连接管理:在多线程环境下(如 Web 应用),每个线程需要独立的数据库连接,在框架中(如
Spring
)通常已内置实现,不建议手写 - 日志追踪:在分布式系统中,通过
ThreadLocal
存储请求链路 ID(如 TraceID),便于日志关联