必须在请求结束时调用 ThreadLocal.remove(),因为 Web 容器(如 Tomcat)复用线程,不清理会导致 value 强引用无法释放、Entry 变为“无效Entry”并持续累积,引发内存泄漏乃至 OOM;唯一稳妥位置是 HandlerInterceptor.afterCompletion() 中配合 try-finally 执行。
为什么必须在请求结束时调用
Web 应用(尤其是基于线程池的 Servlet 容器,如 Tomcat)会复用工作线程处理多个请求。如果只用
而不
,上一个请求存入的变量会一直留在该线程的
中,无法被 GC 回收——这直接导致内存泄漏,且泄漏对象会随请求次数线性增长。
常见错误现象:
且堆 dump 显示大量业务对象被
引用;或者监控发现老年代对象持续不降。
在 Spring MVC 中哪里调用
最稳妥
不能依赖
或普通方法退出——它们不保证执行时机,更不覆盖所有请求路径(比如异常提前退出、异步请求、Filter 链中断)。唯一可靠位置是请求生命周期的明确终点。
使用
:它在视图渲染完成、响应已写出后触发,即使发生异常也会执行(第三个参数
为非 null)
避免用
:此时视图尚未渲染,还可能抛异常,
可能被跳过
若项目用 WebMvcConfigurer + 拦截器注册,确保拦截器添加到所有匹配路径(如
),而非仅特定前缀
的典型误用和边界情况
看似简单的一行调用,实际容易踩坑:
在子线程中调用主线程的
无效——
是线程绑定的,子线程操作的是自己的副本
未配合
:如果
前发生 NPE 或其他未捕获异常,清理逻辑就跳过了;务必包在
块里
静态
实例未设为
:可能导致类卸载失败,加剧内存泄漏
忘记清理嵌套使用的
:比如 A 工具类用了
,B 工具类用了
,两者都得各自
示例(拦截器中):
异步请求或线程切换场景下如何安全清理
Spring 的
、
、消息监听器等都会启用新线程,原始请求线程的
不会自动传递过去,也不能靠拦截器清理——因为新线程根本不在 Web 请求链路里。
不要试图在新线程里调用原
:它对当前线程无意义
若需跨线程传递值,显式传参或使用
(阿里 TTL 库),并在线程任务结束时主动
对于纯异步后台任务(如定时任务),应独立管理其
生命周期,在任务
结束前
关键点在于:每个线程对自己持有的
实例负责,没有“全局清理”机制。
最易被忽略的是:同一个
实例在不同线程中是完全隔离的,清理动作必须发生在它被 set 的那个线程内;而 Web 容器线程复用特性让这个“那个线程”变得隐蔽且持久。
ThreadLocal.remove()ThreadLocal.set()remove()ThreadLocalMapOutOfMemoryError: Java heap spaceThreadLocalMap$Entryremove()@PreDestroyHandlerInterceptor.afterCompletion()expostHandle()remove()/**ThreadLocal.remove()ThreadLocal.remove()ThreadLocaltry-finallyremove()finallyThreadLocalprivate static finalThreadLocaltlAtlBremove()public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
MyContext.getTraceIdTL().remove(); // 假设这是你定义的 static ThreadLocal
MyContext.getUserInfoTL().remove();
} finally {
// 确保执行,哪怕上面 remove 抛异常(极少见但可能)
}
} @AsyncCompletableFutureThreadLocalThreadLocal.remove()TransmittableThreadLocalremove()ThreadLocalrun()remove()ThreadLocalThreadLocal