ThreadLocal 原理与使用详解(二):ThreadLocalMap 结构与弱引用设计解析
在上一篇文章中,我们讲清了 ThreadLocal 的基本使用方式,以及它如何实现线程隔离和上下文传递。但如果只是停留在 API 层面,很容易在实际项目中踩坑——尤其是在配合线程池使用时。
很多人都听说过“ThreadLocal 会导致内存泄漏”,却说不清楚为什么。
- Key 为什么是弱引用?
- Value 为什么不会自动释放?
- static final 到底是最佳实践还是隐藏风险?
这篇文章,我们不再停留在用法层面,而是深入源码,从 ThreadLocalMap 的实现细节出发,拆解它的设计原理,以及背后的内存模型逻辑。ThreadLocal 的操作都是基于 ThreadLocalMap 展开的,而 ThreadLocalMap 是 ThreadLocal 的一个静态内部类,其实现了一套简单的 Map 结构(比 HashMap 简单)。
1. ThreadLocalMap 的主要成员变量
static class ThreadLocalMap {
//Map的条目类型,一个静态的内部类
// Entry继承子WeakReference,Key为ThreadLocal实例
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//Map的条目初始容量16
private static final int INITIAL_CAPACITY = 16;
//Map的条目数组,作为哈希表使用
private Entry[] table;
//Map的条目数量
private int size = 0;
//扩容因子
private int threshold;
}
ThreadLocal源码中 get()、set()、remove() 方法都涉及 ThreadLocalMap 的方法调用,主要调用了ThreadLocalMap 的如下几个方法:
- set(ThreadLocal<?> key, Object value) :向 Map 实例设置 “Key-Value对”。
- getEntry(ThreadLocal):从 Map 实例获取 Key(ThreadLocal实例)所属的 Entry。
- remove(ThreadLocal):根据 Key(ThreadLocal实例)从 Map 实例移除所属的 Entry。
这里只对 ThreadLocalMap 的 set(ThreadLocal<?> key, Object value) 方法的代码以注释的形式做一个简单的分析,具体如下:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根据key的HashCode,找到key在数组上的槽点i
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
//找到现有槽点:Key值为ThreadLocal实例
if (e.refersTo(key)) {
e.value = value;
return;
}
//找到异常槽点:槽点被GC掉,重设Key值和Value值
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
//没有找到现有的槽点,增加新的Entry
tab[i] = new Entry(key, value);
//设置ThreadLocal数
int sz = ++size;
//清理Key为null的无效Entry
//没有可清理的Entry,并且现有条目数量大于扩容因子值,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
2. Entry 的 Key 需要使用弱引用
Entry 用于保存 ThreadLocalMap 的 “Key-Value” 条目,但是 Entry 使用了对 ThreadLocal 实例进行包装之后的弱引用(WeakReference)作为 Key,其代码如下:
// Entry继承了WeakReference,并使用WeakReference对Key进行包装
static class Entry extends WeakReference<ThreadLocal<?>> {
//值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); //使用WeakReference对Key值进行包装
value = v;
}
}
为什么 Entry 需要使用弱引用对 Key 进行包装,而不是直接使用 Threadlocal 实例作为 Key 呢?这个问题有点儿复杂,要分析清楚还有点难度。这里从一个简单的例子入手,假设有一个方法 funcA() 创建了一个“线程本地变量”,具体如下:
public void funcA(){
//创建一个线程本地变量
ThreadLocal local = new ThreadLocal<Integer>();
//设置值
local.set(100);
//获取值
local.get();
//函数末尾
}
当线程 tn 执行 funcA() 方法到其末尾时,线程 tn 相关的 JVM 栈内存以及内部 ThreadLocalMap 成员的结构大致如下图所示:

线程 tn 调用 funcA() 方法新建了一个 ThreadLocal 实例,使用 local 局部变量指向这个实例,并且此 local 是强引用;在调用 local.set(100) 之后,线程 tn 的 ThreadLocalMap 成员内部会新建一个 Entry 实例,其 Key 以弱引用包装的方式指向 ThreadLocal 实例。
当线程 tn 执行完 funcA() 方法后,funcA() 的方法栈帧将被销毁,强引用local 的值也就没有了,但此时线程的ThreadLocalMap 中对应的 Entry 的 Key 引用还指向了 ThreadLocal 实例。如果 Entry 的 Key 引用是强引用,就会导致 Key 引用指向的 ThreadLocal 实例及其 Value 值都不能被 GC 回收,这将造成严重的内存泄漏,具体如下图所示:

什么是弱引用呢?仅有弱引用(Weak Reference)指向的对象只能生存到下一次垃圾回收之前。换句话说,当 GC发生时,无论内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。
说明:什么叫作内存泄漏?不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。对于持续运行的服务进程必须及时释放内存,否则内存占用量越来越高,轻则影响系统性能,重则导致进程崩溃。
由于 ThreadLocalMap 中 Entry 的 Key 使用了弱引用,在下次 GC 发生时,就可以使那些没有被其他强引用指向、仅被 Entry 的Key所指向的 ThreadLocal 实例能被顺利回收。并且,在 Entry 的 Key 引用被回收之后,其Entry 的 Key 值变为 null。后续当 ThreadLocal 的 get()、set() 或 remove() 被调用时,ThreadLocalMap 的内部代码会清除这些 Key 为 null 的 Entry,从而完成相应的内存释放。
3. 编程规范推荐使用 static final 修饰 ThreadLocal 对象
编程规范有云:ThreadLocal 实例作为 ThreadLocalMap 的 Key,针对一个线程内所有操作是共享的,所以建议设置 static 修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载,只会分配一次 存储空间,此类的所有实例都会 共享这个存储空间,所以使用 static 修饰 ThreadLocal 就会节约内存空间。另外,为了确保 ThreadLocal 实例的唯一性,除了使用 static 修饰之外,还会使用 final 进行加强修饰,以防止其在使用过程中发生动态变更。参考的实例如下:
//推荐使用static final线程本地变量
private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<Foo>();
说明:以上代码中,为什么 ThreadLocal 实例除了添加 static final 修饰之后,还经常加上了 private 修饰呢?主要目的是缩小使用的范围,尽可能不让他人引用。
凡事都有两面性,使用 static、final 修饰 ThreadLocal 实例也会带来副作用,使得 Thread 实例内部的 ThreadLocalMap 中 Entry 的 Key 在 Thread 实例的生命期内将始终保持为非 null,从而导致 Key 所在的 Entry 不会被自动清空,这就会让 Entry 中的 Value 指向的对象一直存在强引用,于是 Value 指向的对象在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完 static、final 修饰 ThreadLocal 实例,使用完后必须使用 remove()进行手动释放。如果使用线程池,可以定制线程池的 afterExecute() 方法(任务执行完成之后的钩子方法),在任务执行完成之后,调用 ThreadLocal 实例的 remove() 方法对其手动释放,从而使得其线程内部的 Entry 得到释放。
ThreadLocal 的设计并不复杂,但它的行为却很容易被误解。弱引用解决的是 Key 的回收问题,而不是 Value 的生命周期问题;static final 提供了可复用性,却也把清理责任完全交给了开发者;线程池延长了线程生命周期,也就延长了 ThreadLocal 数据的存活时间。ThreadLocal 本质上是一种“线程绑定存储模型”。线程不结束,存储就不会自然结束。这种设计在单次线程模型中是安全的,但在长生命周期线程(如线程池)中,就必须格外谨慎。
更多推荐



所有评论(0)