在学习多线程的过程中,或多或少都听说到ThreadLocal会有内存泄漏的风险,那么具体是为什么?下面来进行详细的解析一下。
提纲:
ThreadLocal是什么
弱引用
内存泄漏
总结
1. ThreadLocal是什么?
public class ThreadLocal<T> {
//内部类,一个map
static class ThreadLocalMap {
//map的具体内容,entry<k,v> 且k是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
//规定了entry的key是ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
}
}
从源码不难看出:
ThreadLocal有一个内部类ThreadLocalMap
ThreadLocalMap的数据结构就是一个Entry数组
而一个Entry就是一个Key - Value键值对,以 ThreadLocal为key,以Object为Value的Map
再来看看Thread的源码
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}即:
ThreadLocal对象,挂载在当前线程中,具体存储在Thread中的成员变量ThreadLocalMap threadLocals中
ThreadLocalMap为一个键值对Map,key为ThreadLocal,value为Object
2. 弱引用
从ThreadLocalMap的源码中static class Entry extends WeakReference<ThreadLocal<?>>,可以看到这里的Key(ThreadLocal)使用了弱引用。
弱引用:当对象不再被任何强引用关联、仅被弱引用持有时,只要发生 GC,该对象就会被回收。
为什么要将key设计成弱引用呢?
来看一下创建了一个TheadLocal对象之后的堆栈引用示意图:

a. 堆栈引用示意图
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}当创建了一个ThreadLocal新对象之后,就会有几个主要的引用:
new ThreadLocal,在堆区中创建一个ThreadLocal对象,线程中的引用指向新创建出来的ThreadLocal对象 A ,如图中“1”
Thread的引用指向线程对象,如图中“2”
线程对象中有threadLocals 指向ThreadLocalMap对象,如图中“3”
ThreadLocalMap中有指向Entry数组的引用,如图中“4”
其中每一个entry,包含一个key和一个value。key指向ThreadLocal对象,如图中“5”
当业务使用完毕,不在需要该ThreadLocal对象,如
ThreadLocal t1 = new ThreadLocal();
t1.set(value);
t1 = null;
b. "t1 = null"
线程栈中的“1”引用断开,且当前作用域中没有任何其他强引用指向该 ThreadLocal
这时候ThreadLocal对象,只有一个弱引用“5”
此时一旦发生 GC,该 ThreadLocal 对象就会被回收

c. GC回收
所以,Key为弱引用的原因:
及时将ThreadLocal对象的空间进行释放,避免内存堆积
如果是key为强引用,即使t1 = null了,只要线程存活,就会一直存在该ThreadLocal对象
3. 内存泄漏
内存泄漏(Memory Leak):
指对象在逻辑上已经不再被程序使用,但由于仍然存在引用链,导致垃圾回收器无法回收该对象,从而使其长期占用内存。
在 ThreadLocalMap 中,Entry 的 key 是 ThreadLocal 的弱引用,GC 时如果外部不再持有 ThreadLocal 的强引用,key 会被回收并变为
null。但 Entry 中的 value 是强引用,只要 Entry 本身仍然存在,value 对象就始终可达,无法被 GC 回收。
因此,当线程长期存活且未显式调用
ThreadLocal.remove()时,key 已经被回收但 value 仍被 ThreadLocalMap 持有,形成“逻辑上无用、但 GC 无法回收”的对象,这就是 ThreadLocal 存在内存泄漏风险的根本原因。
别急,ThreadLocal中对此做了处理,算是一种兜底机制:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.refersTo(key)) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
private int expungeStaleEntry(int staleSlot) {
......
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
}
......
}remove()方法中,会先将key存在的entry进行删除,然后调用expungeStaleEntry()
在expungeStaleEntry()方法中,会将key为null对应的value进行置空,再将entry置空删除
但是,
ThreadLocalMap 的清理是惰性的、被动触发的,只有在再次访问 ThreadLocalMap 时才可能执行(比如remove(),get(),set());
如果线程长期存活但不再访问 ThreadLocalMap,key 为 null 的 Entry 及其 value 将一直滞留,导致内存泄漏,因此必须显式调用 remove()。
我们不能过于依赖这个expungeStaleEntry(),能remove(),还是要remove()。
4. 总结:
因此,ThreadLocal 的内存泄漏风险并非来源于 ThreadLocal 本身
而是由于在线程生命周期较长的场景下,key 被回收而 value 仍被强引用持有所导致。
为了避免 value 泄漏,最佳实践是在使用完 ThreadLocal 后显式调用
remove()方法。