news 2026/7/21 7:55:28

ThreadLocal没remove内存慢慢涨最后OOM了

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ThreadLocal没remove内存慢慢涨最后OOM了

线上服务跑了三天,突然 OOM 挂了。重启,三天后又挂了。

内存 dump 拉下来,用 MAT 一看——堆里有几十万个UserContext对象,全被 ThreadLocal 引着,GC 收不掉。


排查

代码里有个全局的用户信息持有者:

publicclassUserContext{privatestaticfinalThreadLocal<User>CURRENT_USER=newThreadLocal<>();publicstaticvoidset(Useruser){CURRENT_USER.set(user);}publicstaticUserget(){returnCURRENT_USER.get();}}

每个请求进来,Filter 把用户信息塞进去:

UserContext.set(currentUser);chain.doFilter(request,response);

问题在哪?Filter 的doFilter执行完后——没调remove()

线程回到线程池,ThreadLocal 里的 User 对象还挂着。下一个请求如果是另一个用户,ThreadLocal 被set覆盖,旧 User 对象失去了强引用——但 ThreadLocal 内部的 Entry 还在,弱引用的 key 被 GC 了,value 却因为线程没死而一直存在。

这就是 ThreadLocal 内存泄漏的标准剧本。


根因:ThreadLocal 的内存模型

ThreadLocal 不存数据——数据存在 Thread 对象里的ThreadLocalMap。每个 Thread 有一个 Map,key 是 ThreadLocal 的弱引用,value 是你塞进去的对象。

Thread → ThreadLocalMap → Entry[弱引用→ThreadLocal, 强引用→User]

线程跑完回到线程池,线程没死 → ThreadLocalMap 没清 → Entry 还在。GC 回收了 ThreadLocal 对象本身(因为是弱引用),但value(User)是强引用,不收。

这个 value 就永远挂在 Thread 的 ThreadLocalMap 里,直到线程销毁。


正确写法:finally 里 remove

try{UserContext.set(currentUser);chain.doFilter(request,response);}finally{UserContext.remove();// ✅ 必须清}

remove()会删除 ThreadLocalMap 里对应的 Entry,value 失去引用,可以被 GC 回收。

任何地方用了ThreadLocal.set(),必须在 finally 里调remove()没有例外。


ThreadLocal 另外两个坑

坑一:线程池复用,读到上一个任务的脏数据

ThreadLocal<String>traceId=newThreadLocal<>();// 任务1traceId.set("req-001");// 忘记 remove// 任务2(同一个线程)Stringid=traceId.get();// → "req-001",脏数据

日志里两个不同请求的 traceId 一样——排查链路全乱。

坑二:父子线程传递用 InheritableThreadLocal,线程池不生效

InheritableThreadLocal<String>tl=newInheritableThreadLocal<>();tl.set("parent-value");newThread(()->{System.out.println(tl.get());// ✅ "parent-value"}).start();

InheritableThreadLocal在创建子线程时会把父线程的值拷过去。但线程池里线程是复用的,只在第一次创建时拷贝一次,后面不会更新。

线程池场景用阿里的TransmittableThreadLocal

TransmittableThreadLocal<String>tl=newTransmittableThreadLocal<>();tl.set("value");executor.execute(TtlRunnable.get(()->{System.out.println(tl.get());// ✅ 每次都能拿到最新值}));

ThreadLocal 三个原则:

  1. 用了set就必须在 finally 里remove
  2. 线程池场景更要remove——线程不销毁,泄漏累积
  3. 父子线程传递用TransmittableThreadLocal,别用InheritableThreadLocal

四行代码的事,忘了就是 OOM。


关于作者:无羡,独立开发者,全栈工程师,专注 AI 应用与微服务架构。

📌 分类:技术复盘

🔗 个人博客 — 更多技术文章
✨ 开发者福利整理 — 云服务资源汇总
📂 软件工坊 — 我的技术分享
🌐 个人门户 — 独立开发作品全集

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/21 7:51:45

信奥赛C++顺序结构核心:计算圆问题全解与GESP/CSP-J考点精析

这次我们来看一个信奥赛C基础教程中的核心练习题——计算圆的相关问题。这道题是“顺序结构”章节的典型代表&#xff0c;也是GESP、CSP-J/S等信奥赛入门级考试的常见考点。很多初学者在接触编程时&#xff0c;第一个有成就感的程序可能就是计算圆的面积或周长&#xff0c;但信…

作者头像 李华
网站建设 2026/7/21 7:50:32

全网最牛卸甲AI

全网最牛卸甲AI 超级AI图片魔改视频生成器 怎么想怎么输入提示词指令即可自动生成你想要的图片或视频&#xff01; 重要的是"怎么想的都可以生成"无限制&#xff0c;懂的都懂&#xff01; 而且可以永久免费使用&#xff01; 全网仅此一款&#xff01; 包教会包…

作者头像 李华
网站建设 2026/7/21 7:46:36

教育邮箱申请与AI工具验证实战:解锁Dify与Claude高级功能

最近在折腾 Dify 和 Claude 这类 AI 应用时&#xff0c;很多朋友都卡在了“身份验证”这一关。无论是 Dify 的邮箱验证&#xff0c;还是 Claude 这类海外服务的注册&#xff0c;一个稳定、可信的邮箱地址往往是开启所有高级功能的第一步。特别是对于学生、研究者或预算有限的开…

作者头像 李华
网站建设 2026/7/21 7:45:52

TurboQuant算法:bit级无损量化技术解析与应用

1. TurboQuant算法技术解析TurboQuant算法的核心突破在于实现了bit级别的无损量化&#xff0c;这不同于传统的8-bit或4-bit量化方法。传统量化通常会导致精度损失&#xff0c;而TurboQuant通过创新的权重分组和动态范围调整技术&#xff0c;在保持模型精度的同时实现了显著的加…

作者头像 李华
网站建设 2026/7/21 7:45:12

教育项目成功之道:三维评估体系与动态课程研发

1. 项目背景与核心价值解析 "虎丘江南里"这个教育项目能在区域竞争中稳居前三&#xff0c;绝非偶然。作为深耕教育行业十余年的观察者&#xff0c;我仔细研究过这个项目的运营模式&#xff0c;发现其成功背后有一套完整的教育方法论支撑。不同于传统培训机构单纯追求…

作者头像 李华