Entry的key是弱引用旨在防止ThreadLocal实例泄漏,但因value为强引用且线程长期存活,导致key为null后value无法回收而引发内存泄漏。

ThreadLocalMap 中 Entry 的弱引用设计,本意是防止 ThreadLocal 对象本身泄漏,结果却成了 value 泄漏的导火索——关键不在“弱”,而在“key 弱、value 强”这种不对称结构。
Entry 的 key 为什么是弱引用?
Entry 继承 WeakReference
但要注意:弱引用只作用于 key,对 value 完全没影响。value 始终是强引用,只要线程活着,value 就卡在引用链里出不去。
泄漏是怎么发生的?
一旦 key 被 GC 回收,Entry 就变成 key == null、value != null 的“脏条目”。这种条目有三个致命特点:
- 无法通过 get() 或 set() 访问(操作都依赖非空 key)
- 不会自动从 ThreadLocalMap 数组中移除
- 清理只在 get()/set()/remove() 调用时顺带触发,且仅扫描局部哈希槽,不保证清干净
在线程池场景下尤其危险:线程复用、长期存活,但业务可能只在初始化时 set() 一次,之后再无任何 ThreadLocal 操作。这时脏条目就永远卡着,value(比如大 byte[]、Connection、UserContext)持续占堆,最终 OOM。
为什么 remove() 必须写在 finally 里?
remove() 是唯一能主动切断 value 强引用链的操作。它会:
- 定位到对应 Entry
- 把 value 设为 null
- 调用 clear() 清空 key(即 referent)
- 触发 expungeStaleEntry,清理附近其他脏条目
如果只在 try 块里 remove(),遇到异常就会跳过;写在 finally 才能确保无论是否出错,value 都被释放。Web 应用中常见做法是在 Filter 或 Interceptor 的 finally 中调用 yourThreadLocal.remove()。
static ThreadLocal 真的安全吗?
声明为 static,确实让 ThreadLocal 实例生命周期变长,key 不易被 GC 回收,从而避开“key=null + value滞留”的典型路径。但这只是转移风险,不是根治:
- value 本身若持有大对象或资源(如流、连接),仍需手动清理
- 在容器热部署场景(如 Tomcat reload),static ThreadLocal 可能导致 ClassLoader 泄漏
- Spring Bean 中不要 @Autowired 注入 ThreadLocal 实例,应封装成 static 工具类访问
本质上,ThreadLocal 本身不泄漏,泄漏的是你放进它的 value。设计上它把“谁来负责清理”的责任交给了使用者,而不是 GC。










