当前位置: 首页 > news >正文

ThreadLocal 原理与内存泄漏

ThreadLocal很容易被一句话讲浅:它是线程本地变量。

这句话没错,但不够。面试里真正要讲清楚的是:

  1. 它解决什么问题。
  2. 值到底存在哪里。
  3. 为什么会有内存泄漏风险。
  4. 为什么用完要remove()

ThreadLocal 解决什么问题

ThreadLocal的核心作用是:让每个线程都有自己独立的一份变量副本,避免多个线程争用同一份共享变量。

比如 JDBC 场景里,每个线程可以把自己的Connection放到 ThreadLocal 中。这样线程 A 不会误用或关闭线程 B 的连接。

ThreadLocal

线程 A

线程 B

线程 C

资源副本 A

资源副本 B

资源副本 C

它也常用于保存线程上下文,比如登录用户信息、traceId、租户信息等。

但注意,ThreadLocal不是用来解决所有线程安全问题的。它解决的是“每个线程各用各的”这一类问题。如果多个线程本来就需要共同修改同一份数据,ThreadLocal不适合。

基本用法

PPT 里的例子很直接:

staticThreadLocal<String>threadLocal=newThreadLocal<>();publicstaticvoidmain(String[]args){newThread(()->{Stringname=Thread.currentThread().getName();threadLocal.set("itcast");print(name);System.out.println(name+"-after remove : "+threadLocal.get());},"t1").start();newThread(()->{Stringname=Thread.currentThread().getName();threadLocal.set("itheima");print(name);System.out.println(name+"-after remove : "+threadLocal.get());},"t2").start();}staticvoidprint(Stringstr){System.out.println(str+" :"+threadLocal.get());threadLocal.remove();}

常用方法就三个:

方法作用
set(value)设置当前线程对应的值
get()获取当前线程对应的值
remove()移除当前线程对应的值

值到底存在 ThreadLocal 里吗

这是关键点。

值不是存在ThreadLocal对象自己里面,而是存在当前线程对象的ThreadLocalMap里。

每个Thread内部都有一个ThreadLocalMap。这个 Map 的 key 是ThreadLocal对象,value 是你放进去的线程本地值。

作为 key

Thread-1

ThreadLocalMap

Entry

key: ThreadLocal 弱引用

value: 线程本地值 强引用

ThreadLocal 对象

所以threadLocal.set(value)的真实含义是:

ThreadLocal自己作为 key,把 value 放进当前线程的ThreadLocalMap

set 方法大致怎么走

简化一下源码:

publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){map.set(this,value);}else{createMap(t,value);}}

流程图如下:

存在

不存在

ThreadLocal.set(value)

获取当前线程 Thread

获取线程的 ThreadLocalMap

Map 是否存在

以当前 ThreadLocal 为 key 存 value

创建 ThreadLocalMap

get()也是类似逻辑:

  1. 获取当前线程。
  2. 找到当前线程的ThreadLocalMap
  3. 用当前ThreadLocal作为 key 查 Entry。
  4. 返回 Entry 里的 value。

为什么会内存泄漏

ThreadLocalMap.Entry的 key 是弱引用。

源码形态大概是:

staticclassEntryextendsWeakReference<ThreadLocal<?>>{Objectvalue;Entry(ThreadLocal<?>k,Objectv){super(k);value=v;}}

弱引用的特点是:GC 发现后就可以回收它指向的对象。

问题来了:

如果ThreadLocal对象没有外部强引用了,GC 会把 key 回收掉。于是 Entry 变成:

key = null value = 业务对象

value 还是强引用,仍然被当前线程的ThreadLocalMap持有。

如果这个线程是线程池里的长期存活线程,那 value 也可能长期无法释放。这就是内存泄漏风险。

线程池工作线程长期存活

ThreadLocalMap 长期存在

Entry

key: ThreadLocal 弱引用

value: 业务对象 强引用

ThreadLocal 外部强引用消失

GC 回收 key

key 变成 null

value 仍被 Entry 强引用

内存泄漏风险

这里还有一个面试里常见的追问:

为什么 key 要设计成弱引用?

如果 key 是强引用,ThreadLocalMap会一直强引用 ThreadLocal。即使业务代码不再使用这个 ThreadLocal,它也无法被回收。弱引用至少能让 key 被回收,后续set/get/remove时,Map 有机会清理这些 stale entry。

但这不代表你可以不remove()。因为清理时机不一定马上发生。

为什么用完必须 remove

线程池场景下,线程会被复用。

如果请求 A 在 ThreadLocal 里放了用户 A 的信息,但没有清理,线程回到线程池后又被请求 B 复用。请求 B 可能读到请求 A 的上下文。

这已经不是内存泄漏,而是数据串了。

请求 BThreadLocalMap线程池线程请求 A请求 BThreadLocalMap线程池线程请求 A使用线程set 用户 A请求结束但未 remove复用同一线程get 读到旧值用户 A

正确写法是放在finally

try{USER_CONTEXT.set(user);// do business}finally{USER_CONTEXT.remove();}

只要是 Web 请求、线程池任务、异步任务里的 ThreadLocal,都应该养成这个习惯。

ThreadLocal 和 synchronized 的区别

这两个经常被放在一起问,但它们解决问题的方式完全不同。

对比点synchronizedThreadLocal
思路多线程共享同一份数据时,加锁排队访问每个线程各保存一份数据
解决的问题共享变量并发修改线程隔离、线程上下文
数据是否共享共享不共享
典型场景计数、库存、临界区用户上下文、连接对象、traceId

一句话:

synchronized 是让大家排队用同一份东西,ThreadLocal 是给每个线程各发一份东西。

面试怎么答

可以这样回答:

ThreadLocal用来实现线程隔离。它会让每个线程拥有自己独立的变量副本,避免多个线程争用同一个对象,也可以在线程内共享上下文,比如用户信息、traceId、数据库连接等。

它的值不是存储在ThreadLocal对象本身,而是存储在当前线程的ThreadLocalMap中。ThreadLocalMap的 key 是 ThreadLocal,value 是线程本地变量。

内存泄漏风险来自ThreadLocalMap.Entry的 key 是弱引用,value 是强引用。当 ThreadLocal 没有外部强引用时,key 可能被 GC 回收成 null,但 value 仍然被线程持有。如果线程是线程池里的长期存活线程,value 可能长期无法释放。

所以使用 ThreadLocal 后,尤其在线程池和 Web 请求场景中,一定要在finally里调用remove(),避免内存泄漏和上下文串数据。

http://www.jsqmd.com/news/963435/

相关文章:

  • 深度学习最全入门详解:核心原理、模型分类与应用场景(新手必看)
  • AI Agent时代来临:智能体正在重新定义软件与互联网
  • 数据安全与灾备技术
  • 从VGG16到ResNet18:为什么你的CNN模型不是越深越好?聊聊梯度消失与‘捷径’的诞生
  • PDFtoPrinter:Windows环境下无需PDF阅读器的智能打印解决方案
  • CORDIC算法:用移位与加减实现硬件高效三角函数计算
  • 职教高考优选|合肥理工 2026 官方咨询号码更新发布 - cc江江
  • AI科技热点日报 | 2026年6月6日
  • 如何三步永久保存微信聊天记录?WeChatMsg实用导出与智能分析指南
  • 如何构建高性能WebGL应用:gl-matrix数学库的技术架构解析
  • 2026年杭州AI搜索优化服务商全景评测:从技术到实战的深度选型指南 - 品牌报告
  • 微型压力传感器选购注意事项:广东犸力提醒你别忽视频响带宽与动态响应 - 品牌速递
  • 手把手教你:用qemu-img和vmkfstools搞定KVM虚拟机迁移到ESXi 6.7/7.0(附dracut启动失败修复)
  • SimpleMem:长期记忆不是存得更多,而是让每个 token 更有信息密度
  • 图吧工具箱与自动化运维
  • Hi6001A替代H6911 管脚兼容、内置功率管、待机功耗仅2μA
  • CRT彩电产业供应链重构:从洋垃圾到亿万财富的商业逻辑
  • 2026中检战略合作门店|青岛禹竞名奢汇,依托上金所大盘实时计价结算 - 奢侈品交易观察员
  • 裸眼3D MP4核心技术解析:从DSP算法到定制屏幕的工程实践
  • 如何通过Fast-GitHub插件实现GitHub访问速度10倍提升的突破性解决方案
  • D类功放核心原理与工程实践:从PWM调制到电路调试全解析
  • 从‘说话’到‘摔倒’:手把手教你用SlowFast训练任意自定义动作(附完整配置文件解析)
  • 2026重庆财税咨询机构最新排行:4家合规服务商深度对比 - 奔跑123
  • 利用快马平台十分钟搭建黑马点评项目原型,验证你的产品创意
  • 智搜 GEO 优化系统|手握自研软著,抢占 AI 全域新风口
  • 2026 广东十大除甲醛品牌权威推荐——粤港澳大湾区室内空气治理行业深度测评 - 环保除醛知识库
  • 别再死记DenseNet结构图了!用PyTorch手写一个Dense Block,彻底搞懂它的‘密集’在哪
  • 这么写SQL语句,老板让我明天不用来了!
  • 2026年EPE珍珠棉供应厂家:异形/白色/高密度/精密/水果/汽车零部件EPE专业源头工厂精选 - 品牌企业推荐师(官方)
  • 从零到一:用DDS在C++/Python里实现一个简单的发布订阅聊天室(附完整代码)