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

ThreadLocal 我看了好几遍才看懂,原来关键在引用上

学并发编程绕不过 ThreadLocal,我第一次看的时候觉得"哦,就是给每个线程一份独立的数据嘛",然后就跳过去了。结果后面面试被问到"ThreadLocal 为什么会内存泄漏",直接卡住。

后来我发现,不把底层原理理清楚,光记住"用完了要 remove()"这句话,下次遇到类似的问题还是会懵。

于是又硬着头皮读了一遍源码,这次总算理清楚了。

它到底是干嘛的

先说作用,不然不知道学它干什么。

一个东西给多个线程共用,数据会乱。这是并发编程最原始的问题。解决思路大致有两种:一是用锁让线程排队访问(synchronized、Lock 这些),二是给每个线程一份自己的数据,大家各玩各的,不用抢。ThreadLocal 就是后者。

我遇到的场景是这样的:一个 Web 请求进来,经过拦截器、Controller、Service 好几层,中间可能需要传递用户信息、traceId 这类上下文数据。如果每个方法都通过参数传,代码会变得很啰嗦,改起来也很痛苦。ThreadLocal 可以让你在同一个线程的任意地方取到之前存进去的数据,不用层层传参。

另外还有一个好处——它不用锁。每个线程拿自己的那份副本,天然没有竞争,所以高并发下性能比加锁好很多。

底层结构——它到底把数据存哪了

这一点我最初的理解是错的。我以为值存 ThreadLocal 对象本身里面,后来才发现 ThreadLocal 只是一个"工具人",真正的数据存在当前线程对象(Thread)内部的一个成员变量里。

具体是这样的:

Thread 对象 └── threadLocals(类型是 ThreadLocalMap) └── Entry 数组 ├── Entry 1: key=ThreadLocal实例, value=存的数据 ├── Entry 2: key=ThreadLocal实例, value=存的数据 └── ...

也就是说,每个 Thread 对象内部有一个叫threadLocals的 Map,这个 Map 的 key 是 ThreadLocal 实例本身,value 是你存进去的数据。

所以同一个线程里可以有很多个 ThreadLocal 变量,每个 ThreadLocal 在 Map 里是一条记录。

Entry 的设计——我在这里卡了很久

ThreadLocalMap 里面的 Entry 继承了WeakReference<ThreadLocal<?>>。这句话我第一次看的时候没多想,后来发现这才是整套设计最核心、也是最有争议的地方。

Entry 的 key 是弱引用(WeakReference),而 value 是强引用

弱引用的意思是:如果这个对象只被弱引用指向,没有被其他地方强引用,那么下次 GC 就可以回收它。

为什么要用弱引用?我当时想了很久。后来理解的是这样:

假设我们有一个ThreadLocal<UserInfo> userLocal = new ThreadLocal<>(),用完数据之后,我们把userLocal = null置空了。如果没有弱引用,Entry 里的 key 还强引用着这个 ThreadLocal 对象,那 GC 就永远回收不了它。有了弱引用,userLocal = null之后,下一次 GC 就能把 ThreadLocal 对象回收掉,key 变成 null。

但问题在于:key 被回收了,value 还在啊。value 是强引用,只要 Entry 还挂在 ThreadLocalMap 里,而 ThreadLocalMap 又被 Thread 强引用着,那这个 value 就永远释放不掉。尤其是在线程池的场景下——线程是长期存活、反复利用的,Thread 对象一直活着,这条 Entry 和它的 value 就一直不会释放。

这就是 ThreadLocal 内存泄漏的根源。

怎么解决

官方给的解决办法很直接:用完了,显式调用remove()

try{userLocal.set(userInfo);// 执行业务逻辑}finally{userLocal.remove();}

remove()方法会找到当前线程的 ThreadLocalMap,以当前 ThreadLocal 为 key,把对应的 Entry 整条删掉。这样 Entry 被移除了,value 也就没有引用指向它了,可以被正常回收。

另外,ThreadLocalMap 的get()set()方法内部也做了一些"顺手清理"的工作——它们遍历的时候如果发现 key 为 null 的脏 Entry,会顺手清理掉。但这只是辅助,不能完全指望它。

还有一个有意思的点——哈希冲突的处理方式

ThreadLocalMap 解决哈希冲突的方式跟 HashMap 不一样。HashMap 用的是链表+红黑树,而 ThreadLocalMap 用的是线性探测法

线性探测什么意思?就是算出来应该放在索引 5 的位置,结果 5 已经被占了,那就看 6、7、8……直到找到一个空位。取的时候也类似,算出来位置,如果 key 不是目标对象,就往后一个个找。

这种方式的缺点是,如果 Map 里的数据多了,探测链就会变长,查找效率下降。所以单个线程里不要搞太多 ThreadLocal 变量。

不过 ThreadLocal 在哈希值的生成上做了一些优化——它用了一个叫0x61c88647的魔法数来累加哈希值,这个数跟斐波那契数列有关,能让哈希值在 2 的幂次方大小的数组中分布得非常均匀,尽量减少冲突。

总结一下我学到的

  • ThreadLocal 的核心价值是给每个线程一份独立的数据副本,不用锁也能线程安全
  • 数据存在 Thread 内部的 ThreadLocalMap 里,ThreadLocal 只是 key
  • Entry 是"弱 key + 强 value"的设计,这是内存泄漏的根源
  • 解决办法很简单:finally 块里调 remove(),这是铁律,没得商量
  • 底层用线性探测法解决哈希冲突,内部通过魔法数做了优化

学这个东西的过程中我最大的感受是:很多框架和工具类的"最佳实践"背后都有它存在的理由。如果只是记住"用完了要 remove",下次遇到"为什么线程池用了 ThreadLocal 会内存泄漏"还是不会。只有把引用链走一遍,才知道那条链路到哪一步断了、到哪一步没断。

上面哪里写得不准确,欢迎指出——毕竟我也是慢慢看代码看明白的。

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

相关文章:

  • Adobe-GenP 3.0终极指南:如何免费解锁Adobe全家桶所有功能
  • 基于SMAC与HCS08的嵌入式无线开发实战:从环境搭建到产品优化
  • 打破苹果硬件限制:OpenCore Legacy Patcher让老旧Mac重获新生
  • VMware Player Pro停更预警!:2024年起仅限个人非商业使用——Workstation Pro成唯一合规生产环境选择
  • 基于MPC5744P的功能安全评估套件:硬件架构与软件开发实战
  • 多核DSP性能分析实战:硬件跟踪点与计数器点精准定位瓶颈
  • N_m3u8DL-CLI-SimpleG:图形化界面让M3U8视频下载不再困难
  • 5步实战:开源中文字体从痛点分析到完美应用的完整解决方案
  • FFmpegGUI:告别命令行恐惧,3步实现专业级视频处理
  • SCF5250 IEC958接口CD子码解析实战:从寄存器操作到稳定数据流处理
  • APMCM亚太杯数学建模竞赛:从零到一掌握论文写作与团队协作全攻略
  • 嵌入式技术趋势
  • 前端工具链实践
  • 【VMware Workstation Pro 17 vs VirtualBox 7.0】:内存占用差3.8倍、快照启动慢62%、USB 3.0兼容率仅41%…这些硬伤你还在忍?
  • 别让帮助中心变成摆设:用AI知识管理构建产品在线帮助中心的5个关键步骤
  • 为什么数据安全评估师突然火了?我扒了今年几十个岗位JD发现...
  • 线下会场网络差?这款APP离线录音不遗漏任何内容
  • S12XE MCU内部锁相环(IPLL)配置实战:从原理到代码避坑指南
  • Python驱动乐高机器人:树莓派Build HAT从入门到实战
  • SCF5250嵌入式系统性能优化:指令缓存、SRAM与SDRAM控制器配置实战
  • SCF5250 I2C寄存器编程实战:从协议到驱动开发避坑指南
  • 嵌入式驱动开发实战:Motorola DSP5685x平台TOD与Button驱动详解
  • 记一次由「 HTTP-2的流优先级(Stream Priority)」未生效的排查
  • 智能传感器APP开发:从设备配网到动作识别全解析
  • Motorola M68HC08电机控制SDK实战:从硬件抽象到ioctl接口设计
  • VinXiangQi:基于YOLOv5的中国象棋AI辅助工具终极指南
  • AI写专著必备!掌握AI专著生成技巧,一键产出20万字专业专著
  • 当测试工程遇到 AI Agent:测试智能体落地实践
  • DSP5685x GPIO与HI驱动开发实战:从硬件抽象到高效通信
  • Keyviz完全指南:从键盘操作透明化到高效协作的革命