必须每次用完在try-finally中主动remove(),因ThreadLocal的key弱引用、value强引用,线程池中不清理会导致内存泄漏;避免static存大对象;推荐线程池统一清理或SafeRunnable封装。

核心就一条:每次用完必须主动 remove(),且必须放在 try-finally 块里。这不是可选项,是线程池或长生命周期线程下的硬性要求。
必须在 finally 中调用 remove()
ThreadLocal 的值(value)是强引用,而 key(ThreadLocal 实例)是弱引用。一旦外部不再持有 ThreadLocal 引用,key 会被 GC 回收,但 value 仍卡在 ThreadLocalMap 里,无法访问也无法释放——这就是泄漏的根源。
所以不能依赖“线程结束自动清理”,因为线程池里的线程根本不会结束。
- 所有 set() 或 get() 后,只要业务逻辑执行完毕,就要立即 remove()
- 必须写在 finally 块中,否则异常一抛,清理就跳过了
- 示例写法:
try {
threadLocal.set(data);
// 执行业务处理
} finally {
threadLocal.remove(); // 这行不能省,也不能挪到 if 或 catch 里
}
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
避免 static ThreadLocal 存大对象
static 本身不直接导致泄漏,但它让 ThreadLocal 实例长期存活;如果它存的是 byte[]、List、DTO 等大对象,每个线程副本都会长期驻留堆内存。
- 不要把用户上下文、数据库连接、缓存数据塞进 static ThreadLocal
- 若必须用,优先存轻量标识(如 userId 字符串),再按需查真实对象
- 尤其警惕 initialValue() 里 new 大对象的写法
在线程池中统一清理更可靠
靠每个任务手动写 try-finally 容易遗漏。推荐两种工程化方案:
- 继承 ThreadPoolExecutor,重写 afterExecute(),在其中遍历已知 ThreadLocal 并调用 remove()
- 封装 SafeRunnable,在 run() 执行前后自动完成 set → 业务 → remove 流程
- Spring 环境下可用 @Scope("prototype") + AOP 拦截,或使用 RequestContextHolder 的清理机制
修复后一定要验证效果
改完代码不等于问题消失。得用真实手段确认是否还有残留:
- 触发一次 Full GC 后,用 jmap -histo 查 ThreadLocalMap 实例数是否明显下降
- 用 MAT 打开 heap dump,搜索 java.lang.ThreadLocal$ThreadLocalMap,检查其 value 字段是否还挂着大量业务对象
- 压测后观察老年代内存曲线,看是否仍有缓慢上涨趋势
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










