大家都知道用完要 remove,但面试官问的是“为什么”。
你连引用链都画不出来,remove 也只是死记硬背。
01 先搞清楚:ThreadLocal 存在哪?
ThreadLocal 的数据不藏在 ThreadLocal 对象里,而是藏在 Thread 对象 里。
每个 Thread 内部有一个 ThreadLocalMap,这个 Map 的 Key 是 ThreadLocal 实例本身,Value 是你 set 进去的值。
换句话说:每个线程自己持有一份变量副本,互不干扰。
这本身没问题。问题出在 Map 的 Key 的设计上。
02 核心原因:弱引用 Key + 强引用 Value
ThreadLocalMap 的 Key 用的是 弱引用(WeakReference)。
啥意思?当外部不再持有 ThreadLocal 引用时,GC 一来,Key 就被回收了。
但 Value 呢?Value 是 强引用,它还活着。
于是 Map 里出现了一个诡异的状态:Key 被回收了,Value 还赖着不走。
这就是所谓的“脏 Entry”——一个 Key 为 null、Value 还在的条目。Value 引用的对象无法被 GC 回收,内存就这么一点一点漏掉了。
03 等等,JDK 不是做了兜底吗?
确实做了。ThreadLocalMap 在 get、set、remove 操作时,会顺带扫描并清理 Key 为 null 的脏 Entry。
但问题是:如果你既不 get 也不 set 也不 remove,兜底就不触发。
典型场景:线程池。
线程池里的核心线程不会销毁,它被反复复用。上一个任务 set 了 ThreadLocal,任务结束后不 remove,这个线程回到池里继续等。
没人 get,没人 set,没人 remove。脏 Entry 就永远住在那儿。
GC 也拿它没办法——因为线程还活着,ThreadLocalMap 还活着,Value 的强引用链还在。
泄漏就这样发生了。
04 为什么不用强引用 Key?
有人会问:Key 用强引用不就不会断了吗?
没错,但那样更惨。
如果 Key 是强引用,即使你把 ThreadLocal 引用置 null,Map 里的 Key 还死死持有 ThreadLocal 对象,GC 永远回收不了。ThreadLocal 本身和 Value 一起泄漏,比现在还严重。
弱引用至少保证了一点:ThreadLocal 对象本身可以被回收,泄漏的只是 Value。
这是两害相权取其轻的设计选择。
05 怎么防?一个原则就够了
用完必须 remove,写在 finally 里。
try {
threadLocal.set(userContext);
// 业务逻辑
} finally {
threadLocal.remove();
}
别想着“反正请求结束就没了”——线程池的核心线程不死,ThreadLocalMap 就不死,Value 就不死。
remove 是唯一可靠的防御手段,没有之一。
06 终极总结
- ThreadLocal 数据存在 Thread 的 ThreadLocalMap 里,不在 ThreadLocal 本身
- Key 是弱引用,GC 后 Key 变 null,Value 强引用仍在,形成脏 Entry
- 线程池场景下脏 Entry 永远不被清理,导致内存泄漏
- 防御手段:finally 中 remove,没有例外
- 答题公式:存储位置 → 引用链 → 泄漏原因 → 线程池放大 → 防御方案
这道题表面考 ThreadLocal,实际考的是你对 Java引用体系 和 线程生命周期 的理解。
百度面试官问“为什么泄漏”,不是想听“用完要 remove”这种正确但空洞的答案。
他想看你能不能把 弱引用 → Key 被回收 → Value 泄漏 → 线程池放大 这条链路完整画出来。
记住了:面试 不考你知道答案,考你知道答案的来路。
更多底层技术解析,欢迎在 云栈社区 交流探讨。