【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 就会返回半初始化的对象。

相关新闻

最新新闻

DeepChem分子指纹终极指南:ECFP与FCFP如何选择?新手必看!

DeepChem分子指纹终极指南:ECFP与FCFP如何选择?新手必看!

DeepChem分子指纹终极指南:ECFP与FCFP如何选择?新手必看! 【免费下载链接】deepchem Democratizing Deep-Learning for Drug Discovery, Quantum Chemistry, Materials Science and Biology 项目地址: https://gitcode.com/GitHub_Trending…

2026/7/3 19:43:45
3个ExplorerPatcher部署故障的深度诊断与实战解决方案

3个ExplorerPatcher部署故障的深度诊断与实战解决方案

3个ExplorerPatcher部署故障的深度诊断与实战解决方案 【免费下载链接】ExplorerPatcher This project aims to enhance the working environment on Windows 项目地址: https://gitcode.com/GitHub_Trending/ex/ExplorerPatcher ExplorerPatcher作为Windows系统界面自定…

2026/7/3 19:43:45
SPI EEPROM与dsPIC30F硬件设计及数据存储管理

SPI EEPROM与dsPIC30F硬件设计及数据存储管理

1. 项目背景与硬件选型解析在嵌入式系统设计中,非易失性存储方案的选择直接影响产品的可靠性和用户体验。M95M04这颗4Mbit SPI EEPROM与dsPIC30F4011微控制器的组合,为存储用户偏好、日程设置等关键数据提供了理想的硬件平台。M95M04作为STMicroelectron…

2026/7/3 19:43:45
Gemini与Claude视觉创作能力实战对比:生成式AI工具选型指南

Gemini与Claude视觉创作能力实战对比:生成式AI工具选型指南

1. 项目概述:一场关于“视觉创作权”的真实较量2026年春天,我坐在工作室里,面前摊着三台设备:一台在跑Gemini 3.1 Pro生成的5秒海滩视频,一台正用Claude 3.5 Sonnet输出一份带交互节点的“量子计算原理”SVG流程图&…

2026/7/3 19:43:45
微信JS-SDK实现PC网页跳转小程序的Nuxt3实践

微信JS-SDK实现PC网页跳转小程序的Nuxt3实践

1. 项目背景与需求分析最近在开发一个企业官网项目时,客户提出了一个看似简单但实现起来颇有挑战的需求:需要在PC端网页中直接跳转到他们的小程序。这个需求在电商、教育、服务预约类网站中非常常见——当用户在电脑上浏览商品或服务时,能一键…

2026/7/3 19:43:45
NanoClaw:轻量级本地智能体框架,纯离线运行的文档处理助手

NanoClaw:轻量级本地智能体框架,纯离线运行的文档处理助手

1. 项目概述:为什么“本地优先”的轻量级智能体正在成为新刚需最近三个月,我陆续给六家中小团队做过技术咨询,几乎每场都会被问到同一个问题:“有没有一种智能体,不依赖云端API、不上传数据、不绑定厂商、装上就能跑&a…

2026/7/3 19:38:44

周新闻

月新闻