Entry的key是WeakReference是为了防止ThreadLocal对象因强引用链无法被GC回收而导致内存泄漏;Entry继承WeakReference后,当外部强引用消失,ThreadLocal实例可在GC时被回收,使key变为null,从而触发后续expungeStaleEntry清理value。

Entry 的 key 为什么是 WeakReference
因为 Entry 继承自 WeakReference<threadlocal>></threadlocal>,它的 key(即传入的 this,也就是 ThreadLocal 实例)被包装成弱引用。这样设计的直接目的是:当外部不再强引用该 ThreadLocal 对象(比如设为 null 或所在类被卸载),GC 就能回收它,避免 ThreadLocal 实例本身长期滞留内存。
但要注意:弱引用只作用于 key,不影响 value。只要线程还活着(如线程池中的核心线程),ThreadLocalMap 就一直存在,而其中 Entry.value 是强引用——这才是泄漏的起点。
Entry.value 强引用如何导致泄漏
一旦 key 被 GC 回收,Entry 就变成 key == null 但 value != null 的“幽灵条目”。这种条目无法通过 get() 或 set() 访问(因为操作都依赖非空 key),但又不会自动消失:
-
ThreadLocalMap不会在 GC 后主动扫描并清理这些条目 - 清理只在
get()、set()、remove()时,顺带探测并驱逐 key 为null的 Entry(称为“启发式清理”) - 如果某个线程长期不调用这些方法(例如只初始化一次后就闲置),这些
value就永远卡在 map 里
典型后果:存了 new byte[10 * 1024 * 1024] 这种大对象,线程复用下,每个线程的 map 都积压若干个,很快 OOM。
为什么 remove() 必须写在 finally 块里
不是“建议”,而是防止异常绕过清理逻辑的硬性要求。看这个常见反模式:
threadLocal.set(data); doSomethingThatMightThrow(); // 如果这里抛异常,remove 就不会执行 threadLocal.remove();
正确写法必须是:
try {
threadLocal.set(data);
doSomethingThatMightThrow();
} finally {
threadLocal.remove(); // 即使异常也保证执行
}
还要注意:remove() 清理的是当前线程 map 中对应 key 的整个 Entry,包括 key 和 value;它不依赖 key 是否还存活,所以能真正切断 value 的强引用链。
静态 ThreadLocal + 线程池是最危险组合
静态声明的 ThreadLocal 实例生命周期极长(通常与 ClassLoader 同寿),而线程池里的线程几乎永生。这两者叠加,会让“忘记 remove()”的代价被无限放大:
- 每次任务都
set()大对象 → map 新增或覆盖一个 Entry - 没
remove()→ 下次任务来时,旧 value 仍被强引用着 - 即使 key 是弱引用,也根本轮不到它被回收——因为静态变量始终强引用着那个
ThreadLocal实例
也就是说,这种场景下弱引用完全失效,泄漏完全由 value 强引用 + 线程不退出共同锁定。这也是线上最常触发 OOM 的 ThreadLocal 使用方式。

















