ThreadLocal 用完为什么要 remove?除了避免下一次任务读到旧值,还涉及线程长期存活时的引用保留。
弱引用只用在 key 上
JDK 8 的 ThreadLocalMap.Entry 以弱引用保存 ThreadLocal 这个 key,但 value 是普通的强引用。
当外部不再引用某个 ThreadLocal,它可以被垃圾回收,Entry 的 key 随后变成 null。然而只要线程、Map 和 Entry 仍然可达,value 就可能继续被引用。
存活的 Thread -> ThreadLocalMap -> Entry -> value
└─ 弱引用 key所以“key 是弱引用”不等于“value 自动同时被回收”。反过来,如果 ThreadLocal 是一个仍然可达的静态字段,key 根本没有消失,但 value 依然可能因为忘记 remove 而长期保留。
在线程池里看旧值
先看线程复用时会发生什么:前一个任务设置的值,后一个任务还能读到。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalLeakDemo {
private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newSingleThreadExecutor();
try {
pool.submit(() -> LOCAL.set("task-a")).get();
pool.submit(() -> {
try {
System.out.println(LOCAL.get());
} finally {
LOCAL.remove();
}
}).get();
pool.submit(() -> System.out.println(LOCAL.get())).get();
} finally {
pool.shutdown();
}
}
}输出 task-a 和 null。单线程池复用了工作线程,第一次任务结束并没有清掉 ThreadLocal,第二个任务仍然读到了它。
这个例子为了展示问题,故意在第一个任务里漏掉 remove。实际使用时,应当在第一个任务自己的 finally 中调用 remove。
Map 会不会自己清理
源码里有 expungeStaleEntry、cleanSomeSlots 等清理逻辑,get/set/remove 的某些路径会扫描并清理 key 已失效的条目。
但它不是一个持续运行的后台回收器。线程如果长期空闲,或者后续访问没有触发相关清理,失效条目的 value 仍可能保留一段很长时间。不能把“有清理逻辑”理解成“业务不用 remove”。
正确清理的位置
清理必须发生在保存值的那个线程上。主线程调用 remove,清的是主线程自己的映射,不是线程池工作线程的映射。
任务的入口负责 set,出口的 finally 负责 remove,边界最清楚。如果存在嵌套上下文,还要考虑内层结束后恢复外层的旧值,不能直接把外层的上下文一起清掉。
内存泄漏的关键是本来不再需要的对象仍被持有。排查时沿着 GC Root 到 value 的引用链看,比仅凭“用了 ThreadLocal”就判断有泄漏更准确。