先说结论
ThreadLocal 为同一变量提供每线程独立副本,数据存在线程对象内部的 ThreadLocalMap。Map 的键是 ThreadLocal 弱引用,值仍是强引用;键被回收后,如果线程长期存活且没有触发清理,值可能继续滞留。
在线程池中线程反复复用,不清理还会把上一个请求的用户、租户或链路信息泄露给下一个任务。
关键机制
get/set/remove 会顺便清理一部分失效槽位,但这不是及时回收保证。ThreadLocal 的隔离边界是线程,不是请求;异步切换线程后上下文也不会自动跟随。
ThreadLocalMap 的结构
每个 Thread 持有一个 ThreadLocalMap,而不是 ThreadLocal 持有每个线程的数据。Map 的 Entry 继承弱引用,弱引用指向 ThreadLocal key,value 则是强引用;哈希冲突使用开放寻址探测。
当业务代码不再引用某个 ThreadLocal,GC 可回收 key,但线程池工作线程仍活着,Entry 中的 value 可能继续占用内存。后续 get/set/remove 可能顺带清理该 stale entry,然而没有后续访问时它可能长期存在。
请求上下文的标准写法
final class RequestContextHolder {
private static final ThreadLocal<RequestContext> CONTEXT = new ThreadLocal<>();
static void with(RequestContext context, Runnable action) {
CONTEXT.set(context);
try {
action.run();
} finally {
CONTEXT.remove();
}
}
}
把 set/remove 封装在单个边界组件中,比让控制器、拦截器和业务代码分别管理更可靠。异常、提前 return、任务超时和框架重试都不会绕过 finally。
异步场景为什么会丢上下文
String traceId = TRACE_ID.get();
executor.execute(() -> log.info("trace={}", TRACE_ID.get()));
任务在线程池的另一个线程执行,后者有自己的 ThreadLocalMap,所以默认取不到 traceId。解决方法是显式把不可变上下文传入任务、通过 TaskDecorator 包装提交动作,或使用符合框架规范的上下文传播库;传播后同样要清理,避免污染工作线程。
不要用 InheritableThreadLocal 解决线程池问题。它只在创建子线程时复制父线程值,而池中工作线程通常早已创建,且复制可变上下文会产生更隐蔽的共享问题。
ThreadLocal 与虚拟线程
虚拟线程也支持 ThreadLocal,但“一请求一虚拟线程”可能让每个请求都创建 ThreadLocalMap。轻量上下文、显式参数和 ScopedValue 等更结构化的机制在新代码中值得评估,尤其是只读、作用域明确的上下文。
线上排查清单
内存长期上涨且 Full GC 后无法回落时,除了业务缓存,要检查线程池数量、线程生命周期、ThreadLocal value 是否保存大对象或类加载器。堆转储中从 Thread -> threadLocals -> Entry.value 的引用链是重要证据;修复后还应压测验证工作线程复用下不再串上下文。
可以怎么用
context.set(value);
try {
handle();
} finally {
context.remove();
}
尽量缩小生命周期,不存放体积很大的对象。跨线程传递应显式封装上下文,或使用框架提供的传播机制并确保清理。
容易踩坑的地方
ThreadLocal 不是解决共享变量竞争,而是避免共享。InheritableThreadLocal 在线程池中也不可靠,因为线程通常早于请求创建,继承发生在创建线程时。
常见问题
追问:为什么 key 要设计成弱引用?
这样调用方不再持有 ThreadLocal 时,键有机会被回收;但值的清理仍依赖 Map 后续操作,因此显式 remove 仍不可省略。
追问:static final ThreadLocal 会被回收吗?
通常不会,因为类一直强引用它。只要 value 生命周期和线程生命周期都受控,这种模式反而避免了“key 被回收、value 残留”的一类问题,但每次请求仍必须 remove。
追问:能把数据库连接放到 ThreadLocal 吗?
框架可以在清晰事务边界内绑定连接,但业务代码自行长期保存连接风险很大:线程池复用会串请求,连接超时或事务异常也难以正确释放。优先使用数据源和事务框架。
追问:ThreadLocal 能解决线程安全吗?
只能让每个线程看到独立副本,不能协调跨线程共享数据,也不能让同一个对象内部操作原子化。它是隔离工具,不是同步工具。