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

浅聊一下ThreadLocal

大家好,今天咱们来好好聊聊 ThreadLocal—— 这个在多线程开发中超实用,但也容易踩坑的组件。不管是日常开发里的线程数据隔离,还是面试中常被问到的原理和内存泄露问题,本篇文章都会给你讲清楚。

一、ThreadLocal 是什么?一句话说透

ThreadLocal 翻译过来叫 “线程局部变量”,核心作用就两个:

  1. 线程内共享数据:同一个线程里,不同类、不同方法之间可以轻松拿取数据,不用靠参数来回传;
  2. 线程间隔离数据:每个线程存的数据只属于自己,其他线程看不到也改不了,天然避免多线程并发安全问题。

它底层是靠 Thread 类里的 ThreadLocalMap 来存数据的 —— 简单理解就是,每个线程都有自己专属的 “小账本”(ThreadLocalMap),账本里记着该线程通过 ThreadLocal 存的各种数据。

二、ThreadLocalMap 内部结构:到底怎么存数据?

先明确一点:ThreadLocal 本身不存数据,真正存数据的是线程自己的 ThreadLocalMap

1. 核心结构

ThreadLocalMap 底层是一个 Entry 类型的数组,每个 Entry 就是一个键值对:

  • Key:ThreadLocal 对象(注意是弱引用,后面讲内存泄露会用到);
  • Value:我们实际要存的数据(比如用户信息、SqlSession 等)。

关系可以这么理解:
Thread → 有一个 ThreadLocalMap → ThreadLocalMap 里是 Entry[] 数组 → 每个 Entry 对应 “ThreadLocal 对象 + 数据”

三、ThreadLocal 常用方法:3 个核心操作

ThreadLocal 的 API 特别简单,就 3 个常用方法,记熟就行:

1. 存数据:void set (T value)

把数据存到当前线程的 ThreadLocalMap 里。
比如:threadLocal.set(userInfo); → 现在当前线程的 “小账本” 里,就有了这条以该 ThreadLocal 为 Key、userInfo 为 Value 的记录。

2. 取数据:T get ()

从当前线程的 ThreadLocalMap 里拿数据。
比如:UserInfo user = threadLocal.get(); → 自动根据当前 ThreadLocal 对象,找到对应的 Value 并返回。

3. 删数据:void remove ()

这个方法尤其重要! 从当前线程的 ThreadLocalMap 里删除数据。

重点提醒:如果用了线程池(线程会复用),一定要在业务结束后(比如 finally 里)调用 remove()!不然线程放回线程池时,旧数据还在 “小账本” 里,下次其他任务用这个线程时,可能拿到脏数据。

四、常见问题:面试常问的 4 个点

1. 为什么用 ThreadLocal 做 Key?不能用 Thread 吗?

假设一个线程里只用到 1 个 ThreadLocal,用 Thread 当 Key 好像也行(毕竟一个线程对应一个数据)。但实际开发中,一个线程可能会用多个 ThreadLocal(比如同时存用户信息、请求上下文、SqlSession)。

如果用 Thread 当 Key,多个数据就会 “打架”—— 根本分不清哪个数据对应哪个用途。而用 ThreadLocal 当 Key,每个 ThreadLocal 对应一个数据,精准匹配,不会乱。

2. ThreadLocalMap 怎么找数据?(哈希计算逻辑)

当调用 get ()/set () 时,ThreadLocalMap 会通过 哈希计算 找到 Entry 在数组中的位置,步骤很简单:

  1. 拿当前 ThreadLocal 对象的 hashCode
  2. 用 hashCode & (数组长度 - 1) 计算下标(这个操作和 “取余” 效果一样,但效率更高);
  3. 根据下标找到对应的 Entry,进而拿到 Value。

举个例子:数组长度是 16,ThreadLocal 的 hashCode 是 31,那么 31 & 15 = 15 → 就去数组下标 15 的位置找数据。

3. 父子线程怎么共享数据?ThreadLocal 不行!

ThreadLocal 的数据是线程隔离的,父线程存的数据,子线程拿不到 —— 因为父子线程是两个不同的 Thread 对象,各有各的 ThreadLocalMap。

这种场景要换 InheritableThreadLocal,它是 JDK 自带的,继承了 ThreadLocal。原理是:子线程创建时,会把父线程 InheritableThreadLocal 里的数据 “拷贝” 一份到自己的 ThreadLocalMap 里,这样父子线程就能共享数据了。

4. ThreadLocal 怎么避免内存泄露?

先搞清楚为什么会内存泄露:Entry 的 Key 是 ThreadLocal 弱引用,当 ThreadLocal 对象没人用了(比如被 GC 回收),Key 就变成 null 了。但 Value 还是强引用,只要线程没销毁,Value 就一直占着内存,这就是内存泄露。

解决办法就一个:用完 ThreadLocal 后,一定要调用 remove () 方法
remove() 会把 Entry 的 Key 和 Value 都设为 null,这样 GC 就能及时回收这部分内存,从根源上避免泄露。

推荐写法(在 finally 里调用,即使业务抛异常也能清理):

public void doSomething(UserDto userDto) {UserInfo userInfo = convert(userDto);try {// 存数据CurrentUser.set(userInfo);// 业务逻辑UserInfo current = CurrentUser.get();// ...} finally {// 无论如何都要删CurrentUser.remove();}
}

五、ThreadLocal 应用场景:实际开发用在哪?

1. 线程数据隔离:避免并发安全问题

最典型的场景是 SqlSession 绑定。MyBatis 里,每个线程的 SqlSession 是独立的,用 ThreadLocal 存起来,避免多个线程共用一个 SqlSession 导致的关闭异常或数据混乱:

private static final ThreadLocal<SqlSession> threadSession = new ThreadLocal<>();public static SqlSession getSession() {SqlSession s = threadSession.get();if (s == null) {s = getSqlSessionFactory().openSession();threadSession.set(s); // 绑定到当前线程}return s;
}

2. 跨函数传递数据:减少代码耦合

比如在 SpringMVC 里,我们经常需要获取 HttpServletRequest 对象。如果每个方法都靠参数传,太麻烦了。Spring 用 RequestContextHolder 帮我们搞定,底层就是 ThreadLocal:

// 直接获取 Request,不用传参数
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

看 RequestContextHolder 源码就知道,它内部用了两个 ThreadLocal 存请求上下文:

private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");

这样一来,同一个请求(对应一个线程)里,不管在哪个类、哪个方法,都能轻松拿到 Request 对象,代码耦合度大大降低。

总结

ThreadLocal 核心就是 “线程专属小账本”,记住 3 个方法、2 个场景、1 个注意点:

  • 3 个方法:set () 存、get () 取、remove () 删(重点!);
  • 2 个场景:线程数据隔离(防并发)、跨函数传数据(解耦);
  • 1 个注意点:线程池场景一定要在 finally 里调用 remove (),避免内存泄露和脏数据。

掌握这些,不管是开发还是面试,ThreadLocal 这块都没问题了~


文章转载自:

http://obkkeIth.btpzn.cn
http://kiTx1SZb.btpzn.cn
http://i1FmicAT.btpzn.cn
http://hkwdFi7V.btpzn.cn
http://tj9stoIH.btpzn.cn
http://tkfZpUE9.btpzn.cn
http://VrBc6cb2.btpzn.cn
http://tFzwKIWY.btpzn.cn
http://Dfm1o6zQ.btpzn.cn
http://7OqfmKIN.btpzn.cn
http://HzBNX5sw.btpzn.cn
http://t4BzBBd3.btpzn.cn
http://xM3j7CgC.btpzn.cn
http://mcNta9sF.btpzn.cn
http://55u9cahq.btpzn.cn
http://YgYgp3Qy.btpzn.cn
http://H0IkzChL.btpzn.cn
http://uBj1SQsC.btpzn.cn
http://UfA6Rw9W.btpzn.cn
http://ePGL8wg0.btpzn.cn
http://CfIIkWp8.btpzn.cn
http://wkK9lS1h.btpzn.cn
http://PZGDqATI.btpzn.cn
http://kEG7r8mu.btpzn.cn
http://Zu0OdbLv.btpzn.cn
http://5Qlw90ZQ.btpzn.cn
http://9MVJk8FJ.btpzn.cn
http://6jfR3V6i.btpzn.cn
http://aUe5ue97.btpzn.cn
http://t1JRWTMn.btpzn.cn
http://www.dtcms.com/a/378775.html

相关文章:

  • 部署大模型的极简笔记
  • linux面试题记录
  • 深度解码OpenAI的2025野心:Codex重生与GPT-5 APIKey获取调用示例
  • 文献阅读笔记:脉冲神经网络最新文献合集-IV
  • STM32学习路线开启篇:芯片简介与课程简介
  • 第七章 ELK Stack高级应用与集成
  • 认识跨平台UI框架Flutter和MAUI区别,如何选。
  • 9.11-QT-QT的基本使用
  • 线程安全相关的注解
  • [超表面论文快讯-242] PR-微波超四元数涡旋阵列洛书加权锁定成像加密-江南大学王继成、上海科技大学王雄团队
  • 质量特性工程
  • 性能测试-jmeter10-分布式测试
  • Java中方法重写与重载的区别
  • 【代码随想录算法训练营——Day9】字符串——151.翻转字符串里的单词、卡码网:55.右旋转字符串、28.实现 strStr()、459.重复的子字符串
  • 少儿舞蹈小程序(13)作品播放量累加及点赞
  • 【AI知识点】模型训练优化之——混合精度训练
  • 华为考试:HCIE数通考试难度分析
  • OpenHarmony app通过U盘升级配置
  • mobx-miniprogram小程序的数据传输
  • rdKafka驾驭手册:从青铜到王者的异步消息屠龙术
  • Ubuntu\Linux环境中驱动版本配置cudaToolKit
  • 法规变更后研发文档更新不及时该怎么办
  • linux 时间相关的命令
  • ThreadLocal 线程本地变量源码深度解析
  • 虚拟化技术(1):虚拟化技术的演进、挑战与突破
  • AWS strands agents 当智能体作为独立服务/容器部署时,它们无法共享进程内状态
  • 云手机与云游戏之间有什么关系?
  • 数据库学习MySQL系列3、Windows11系统安装MySQL方法二.zip压缩包详细教程
  • 淘宝/天猫按图搜索(拍立淘)item_search_img API接口全解析
  • 存储空间操作