【Java踩坑笔记】22_ThreadLocal用完不remove,内存泄漏在等你 22 | ThreadLocal 用完不 remove内存泄漏在等你摘要线程池场景ThreadLocal设置值后不remove()值会一直保留在线程里导致内存泄漏。ThreadLocalMap的 key 是弱引用但 value 是强引用不主动remove()就无法回收。一、问题现象publicclassThreadLocalLeakTest{privatestaticfinalThreadLocalBigDecimalTHREAD_LOCALnewThreadLocal();publicstaticvoidmain(String[]args){ExecutorServicepoolExecutors.newFixedThreadPool(1);for(inti0;i10;i){pool.submit(()-{THREAD_LOCAL.set(newBigDecimal(99999999999999));// 大对象// ❌ 没调用 THREAD_LOCAL.remove()// 线程池的线程会一直持有这个 BigDecimal});}}}现象内存使用量持续增长GC 无法回收ThreadLocal里的值。二、踩坑现场场景 1Web 请求的上下文信息// ❌ 错误拦截器设置了用户信息但没清理ComponentpublicclassUserInterceptorimplementsHandlerInterceptor{privatestaticfinalThreadLocalUserUSER_CONTEXTnewThreadLocal();OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){UserusergetUserFromSession(request);USER_CONTEXT.set(user);// 设置用户信息returntrue;}// ❌ 没有在 afterCompletion 里调用 USER_CONTEXT.remove()}问题Tomcat 的线程池复用线程用户 A 的请求处理完后线程里还保留着用户 A 的信息。下次请求复用同一个线程时USER_CONTEXT.get()可能拿到用户 A 的数据场景 2日期格式化// ❌ 错误每次 set 新对象旧对象没法回收privatestaticfinalThreadLocalSimpleDateFormatDATE_FORMATThreadLocal.withInitial(()-newSimpleDateFormat(yyyy-MM-dd));publicStringformat(Datedate){returnDATE_FORMAT.get().format(date);// 如果线程池有 200 个线程就有 200 个 SimpleDateFormat 对象一直活着}三、原理解析3.1 ThreadLocal 的内存模型Thread └── ThreadLocalMap threadLocals └── Entry[] table ├── Entry(keyThreadLocal 弱引用, value你 set 的对象) ├── Entry(keyThreadLocal 弱引用, value...) └── ...关键点ThreadLocalMap.Entry的key 是弱引用WeakReferenceThreadLocal?value 是强引用直接引用你set的对象3.2 为什么 value 会泄漏1. 线程池的线程不会销毁一直活着 2. 线程的 ThreadLocalMap 一直活着 3. keyThreadLocal可以被 GC 回收弱引用 4. 但 value 是强引用只要线程活着value 就活着 5. 如果没调用 remove()这个 value 永远无法被回收更可怕的是key 被回收后Entry 变成(null, value)这个 value永远无法被访问到但也无法被回收内存泄漏。3.3 弱引用不是万能药很多人以为“key 是弱引用GC 后会自动清理”。错了弱引用只保证key 可以被 GC 回收但value 不会自动清理。必须手动调用remove()。3.4 ThreadLocal 的正确清理时机请求开始preHandle → ThreadLocal.set(userInfo) ↓ 请求处理controller/service → ThreadLocal.get() 获取用户信息 ↓ 请求结束afterCompletion → ThreadLocal.remove() ✅ 必须在这里清理四、正确写法4.1 在 finally 块里 remove// ✅ 正确用完立即 removeExecutorServicepoolExecutors.newFixedThreadPool(4);pool.submit(()-{try{THREAD_LOCAL.set(newBigDecimal(9999999999));// 业务逻辑doBusiness();}finally{THREAD_LOCAL.remove();// ✅ finally 保证一定执行}});4.2 Web 拦截器在 afterCompletion 里 remove// ✅ 正确Spring 拦截器里清理ComponentpublicclassUserInterceptorimplementsHandlerInterceptor{privatestaticfinalThreadLocalUserUSER_CONTEXTnewThreadLocal();OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){USER_CONTEXT.set(getUserFromSession(request));returntrue;}OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){USER_CONTEXT.remove();// ✅ 请求结束后清理}}4.3 用 try-with-resources 模式Java 8 封装// ✅ 封装 ThreadLocal 的使用自动清理publicclassThreadLocalScopeTimplementsAutoCloseable{privatefinalThreadLocalTthreadLocal;privatefinalTvalue;publicThreadLocalScope(ThreadLocalTthreadLocal,Tvalue){this.threadLocalthreadLocal;this.valuevalue;threadLocal.set(value);}publicstaticTThreadLocalScopeTwith(ThreadLocalTtl,Tvalue){returnnewThreadLocalScope(tl,value);}Overridepublicvoidclose(){threadLocal.remove();// ✅ AutoCloseable 自动调用}}// 使用try(ThreadLocalScope.with(USER_CONTEXT,user)){// 业务逻辑doBusiness();}// 自动调用 close() → remove()4.4 每次使用都初始化不用线程池场景// ✅ 如果不用线程池每次 new Thread 的场景可以不 remove// 因为线程结束ThreadLocalMap 也随之销毁// 但养成 remove 的习惯仍然是最好的五、最佳实践✅ ThreadLocal 使用的 5 条铁律每次set()之后必须在finally里调用remove()Spring 拦截器在afterCompletion里remove()线程池场景必须remove()否则内存泄漏初始化放在try外面remove()放在finally里用ThreadLocal.withInitial()代替手动set()初始化但仍需remove() 如何排查 ThreadLocal 内存泄漏# 1. 用 jcmd 或 jmap 导出堆快照jmap -dump:live,formatb,fileheap.hprofpid# 2. 用 Eclipse MAT 分析# 查找java.lang.ThreadLocal$ThreadLocalMap$Entry# 筛选出 keynull 但 value 不为 null 的 Entry️ IDEA 的 Hints开启ThreadLocal is not removed检查让 IDE 在ThreadLocal.set()后没找到remove()时提醒你。六、小结ThreadLocal的key 是弱引用value 是强引用线程池场景线程不销毁value 会一直积累导致内存泄漏必须养成习惯set()之后在finally里remove()Spring 拦截器里在afterCompletion回调中remove()内存泄漏排查用 MAT 分析堆快照找ThreadLocalMap$Entry中keynull的条目下一篇预告double-checked locking 单例你写的真的线程安全吗—— 看似完美的双重检查锁少了 volatile 就会返回半初始化的对象。

相关新闻

最新新闻

OpenKedge:AI智能体安全治理框架与Agentic Mutation实践

OpenKedge:AI智能体安全治理框架与Agentic Mutation实践

1. 项目概述:当智能体开始“自我进化”,我们如何确保安全?最近,一个名为OpenKedge的概念在技术社区里被频繁讨论。它直指一个正在发生、且越来越紧迫的问题:当AI智能体(Agent)具备了自我修改、自…

2026/8/25 11:29:34
CiA 304详解:CANopen功能安全通信的核心协议

CiA 304详解:CANopen功能安全通信的核心协议

1. 项目概述:为什么CiA 304不是“另一个CANopen补充协议”,而是功能安全落地的关键拼图如果你正在做伺服驱动器、PLC安全模块、电梯控制柜或AGV底盘控制器,又或者你刚接手一个需要通过EN ISO 13849-1 PLd或IEC 61508 SIL2认证的工业设备项目&…

2026/8/25 11:29:34
Minecraft Forge 1.20.1 创造模式物品栏(CreativeModeTab)开发全解析

Minecraft Forge 1.20.1 创造模式物品栏(CreativeModeTab)开发全解析

1. 这不是“加个按钮”那么简单:创造模式物品栏的本质是游戏内UI调度系统你搜“Minecraft Mod 开发:4-创造模式物品栏”,大概率刚写完前三个教程——注册方块、注册物品、处理基础事件,正准备把自制道具塞进创造模式里&#xff0c…

2026/8/25 11:29:34
C++结构体排序:重载运算符、自定义函数与Lambda表达式实战指南

C++结构体排序:重载运算符、自定义函数与Lambda表达式实战指南

1. 从一次数据展示的尴尬说起:为什么结构体排序是基本功最近在帮一个做嵌入式设备日志分析的朋友看代码,他遇到了一个挺典型的问题。设备上报的日志数据包是一个结构体数组,每个结构体包含了时间戳、设备ID、错误码和描述信息。他的需求很简单…

2026/8/25 11:29:34
ZAP HUD实战指南:提升Web渗透测试效率的核心交互层

ZAP HUD实战指南:提升Web渗透测试效率的核心交互层

1. ZAP HUD到底是什么,为什么它让很多刚入门的安全测试人员突然“开窍”ZAP HUD(Head-Up Display)不是汽车挡风玻璃上投射车速的光学设备,也不是什么新出的AR眼镜套件——它是OWASP ZAP官方在2020年正式集成进主程序的一个实时交互…

2026/8/25 11:29:34
从静默死亡到进程崩溃:C# BackgroundService 在信创环境的 5 个夺命坑,你踩过几个?

从静默死亡到进程崩溃:C# BackgroundService 在信创环境的 5 个夺命坑,你踩过几个?

🔥关注墨瑾轩,带你探索编程的奥秘!🚀 🔥超萌技术攻略,轻松晋级编程高手🚀 🔥技术宝库已备好,就等你来挖掘🚀 🔥订阅墨瑾轩,智趣学习不…

2026/8/25 11:24:34