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

InheritableThreadLocal 详解

你已经知道ThreadLocal可以让每个线程拥有自己的"私有变量",互不干扰。但如果遇到父子线程(一个线程里再开一个新线程)的场景,ThreadLocal就会暴露一个大坑——子线程拿不到父线程的值。

InheritableThreadLocal就是来解决这个问题的。


一、ThreadLocal 的痛点:父子线程不共享

先看一个例子,感受一下问题:

public class ThreadLocalDemo { // 普通的 ThreadLocal private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { threadLocal.set("我是主线程的值"); System.out.println("主线程获取: " + threadLocal.get()); new Thread(() -> { System.out.println("子线程获取: " + threadLocal.get()); // 输出 null! }).start(); } }

运行结果:

主线程获取: 我是主线程的值 子线程获取: null

原因ThreadLocal是线程隔离的,主线程和子线程各自有独立的存储空间,子线程创建时不会自动"继承"父线程的值。


二、InheritableThreadLocal 是什么?

InheritableThreadLocalThreadLocal的子类。它的特殊之处在于:子线程创建时,会自动把父线程的值复制一份到自己的空间里。

只改一行代码,把ThreadLocal换成InheritableThreadLocal

public class InheritableThreadLocalDemo { // 换成 InheritableThreadLocal private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { inheritableThreadLocal.set("我是主线程的值"); System.out.println("主线程获取: " + inheritableThreadLocal.get()); new Thread(() -> { System.out.println("子线程获取: " + inheritableThreadLocal.get()); }).start(); } }

运行结果:

主线程获取: 我是主线程的值 子线程获取: 我是主线程的值

子线程成功拿到了父线程的值!


三、更复杂的例子:多级继承

InheritableThreadLocal的继承是可以传递,子线程的值还可以继续传给孙线程:

public class MultiLevelDemo { private static InheritableThreadLocal<String> ctx = new InheritableThreadLocal<>(); public static void main(String[] args) { ctx.set("爷爷的值"); new Thread(() -> { System.out.println("儿子线程: " + ctx.get()); // 爷爷的值 ctx.set("爸爸的值"); // 儿子可以修改自己的副本,不影响爷爷 new Thread(() -> { System.out.println("孙子线程: " + ctx.get()); // 爸爸的值 }).start(); }).start(); // 主线程的值不变 System.out.println("主线程: " + ctx.get()); // 爷爷的值 } }

关键点每个线程拿到的是父线程值的副本,子线程修改自己的值不会影响父线程。


四、底层原理

不用深入源码,理解这两个概念就行:

1. 普通 ThreadLocal 存在哪里?

每个Thread对象内部有一个threadLocals字段(类型是ThreadLocalMap,专门存这个线程的ThreadLocal变量。

2. InheritableThreadLocal 存在哪里?

每个Thread对象内部还有一个inheritableThreadLocals字段

当创建子线程时,JVM 会检查父线程的inheritableThreadLocals,把里面的内容复制一份给子线程。

父线程 ├── threadLocals ← 普通 ThreadLocal 存这里,子线程看不到 └── inheritableThreadLocals ← InheritableThreadLocal 存这里,子线程创建时复制过去 子线程 ├── threadLocals └── inheritableThreadLocals ← 复制了父线程的值

3. 为什么是"复制"而不是"共享引用"?

因为ThreadLocal的设计初衷就是线程隔离。如果父子线程共享同一个引用,那子线程一改,父线程也变了,就违背了隔离原则。所以 JVM 选择复制值

⚠️ 注意:InheritableThreadLocal创建子线程时,会把父线程中的 ThreadLocal 值传递给子线程。默认情况下传递的是对象引用,也就是浅拷贝,所以如果 Value 是可变对象,父子线程实际上可能操作同一个对象,子线程修改对象内部属性后,父线程也能看到这个变化。

4、浅拷贝

核心就是:InheritableThreadLocal复制的是父线程中变量的引用,而不是把对象本身复制一份。

public class InheritableThreadLocalDemo { // 一个可修改的对象 static class User { String name; public User(String name) { this.name = name; } @Override public String toString() { return "User{name='" + name + "'}"; } } // InheritableThreadLocal static InheritableThreadLocal<User> threadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { // 1. 父线程创建 User 对象 User user = new User("张三"); // 2. 放入父线程的 InheritableThreadLocal threadLocal.set(user); System.out.println("父线程修改前:" + threadLocal.get()); Thread child = new Thread(() -> { // 3. 子线程可以获取父线程的 User User childUser = threadLocal.get(); System.out.println("子线程获取到:" + childUser); // 4. 子线程修改 User 对象内部属性 childUser.name = "李四"; System.out.println("子线程修改后:" + childUser); }); child.start(); child.join(); // 5. 父线程再次获取 System.out.println("父线程最终:" + threadLocal.get()); } }

运行结果大概是:

父线程修改前:User{name='张三'} 子线程获取到:User{name='张三'} 子线程修改后:User{name='李四'} 父线程最终:User{name='李四'}

为什么会这样?

关键在这里:

User user = new User("张三"); threadLocal.set(user);

假设内存中:

父线程 InheritableThreadLocal │ ▼ User对象 name="张三"

创建子线程时,InheritableThreadLocal不是重新创建一个 User 对象

而是把这个引用复制给子线程:

父线程 ThreadLocalMap │ │ ▼ User对象 <──────── 子线程 ThreadLocalMap name="张三"

所以实际上:

threadLocal.get()

在父线程和子线程中拿到的是同一个 User 对象

因此子线程执行:

childUser.name = "李四";

实际上修改的是这个共同的 User 对象:

┌───────────────┐ 父线程 ──────────►│ User对象 │ │ name = "李四" │ 子线程 ──────────►│ │ └───────────────┘

所以父线程再次:

threadLocal.get()

拿到的自然也是:

name = "李四"

你要特别注意一个容易混淆的地方

浅拷贝不是说“子线程和父线程的 ThreadLocalMap 是同一个”。

不是。

实际上是:

父线程 ThreadLocalMap │ └── key → User对象 ←──┐ │ 子线程 ThreadLocalMap │ │ │ └── key ──────────────┘

Map 是各自的,但是 Map 里面存的 User 对象引用指向同一个对象。

另外还有一个很重要的点:如果子线程不是修改对象内部属性,而是直接set()一个新的对象,就不会影响父线程的 ThreadLocal 值。


五、注意事项与坑

坑 1:线程池里可能失效

这是最常见的坑!InheritableThreadLocal的值只在创建线程时从父线程复制一次。如果用线程池,线程是复用的,不会重新创建,也就不会重新继承父线程的最新值。

ExecutorService executor = Executors.newFixedThreadPool(2); InheritableThreadLocal<String> ctx = new InheritableThreadLocal<>(); // 第一次提交 ctx.set("第一次的值"); executor.submit(() -> { System.out.println("任务1: " + ctx.get()); // 第一次的值 }); Thread.sleep(100); // 修改值后再提交 ctx.set("第二次的值"); executor.submit(() -> { System.out.println("任务2: " + ctx.get()); // 可能还是"第一次的值"! });

原因:线程池里的线程是之前创建的,已经继承了"第一次的值",不会再次继承"第二次的值"。

解决:用TransmittableThreadLocal(阿里开源的 TTL),或者每次执行任务前手动传值。


坑 2:值修改不同步

子线程修改自己的副本,父线程不会感知;反过来也一样。这是设计如此,不是 bug。

ctx.set("父线程的值"); new Thread(() -> { System.out.println("子线程修改前: " + ctx.get()); // 父线程的值 ctx.set("子线程的值"); System.out.println("子线程修改后: " + ctx.get()); // 子线程的值 }).start(); Thread.sleep(100); System.out.println("父线程: " + ctx.get()); // 还是"父线程的值"

坑 3:内存泄漏

ThreadLocal一样,用完记得remove(),尤其是线程池场景:

try { ctx.set("someValue"); // 执行业务逻辑 } finally { ctx.remove(); // 一定要清理! }

六、常见使用场景

场景说明
用户登录上下文传递Web 主线程里存了当前用户信息,异步处理(开新线程)时子线程需要知道是谁在操作
TraceID/RequestID 传递分布式链路追踪中,主线程生成 TraceID,子线程需要继承以继续打日志
事务上下文主线程开启事务,子线程需要知道当前事务状态
线程级配置传递父线程设置了某些业务参数,子线程默认沿用

七、一张图总结

┌─────────────────────────────────────────────┐ │ InheritableThreadLocal │ ├─────────────────────────────────────────────┤ │ 是什么:ThreadLocal 的子类,支持父子线程传值 │ ├─────────────────────────────────────────────┤ │ 核心能力: │ │ • 子线程创建时,自动复制父线程的值 │ │ • 每个线程仍有独立副本,互不影响 │ ├─────────────────────────────────────────────┤ │ 注意坑点: │ │ • 线程池场景下可能失效(线程复用不重新继承) │ │ • 是浅拷贝,对象引用要注意 │ │ • 用完要 remove(),防止内存泄漏 │ ├─────────────────────────────────────────────┤ │ 线程池替代方案:TransmittableThreadLocal (TTL) │ └─────────────────────────────────────────────┘

八、面试常问

  1. ThreadLocal 和 InheritableThreadLocal 的区别?

    • ThreadLocal:线程隔离,父子线程不共享。

    • InheritableThreadLocal:子线程创建时继承父线程的值,之后各自独立。

  2. InheritableThreadLocal 在线程池里能用吗?

    • 不能直接用,因为线程池复用线程,不会触发"创建时复制"。需要用TransmittableThreadLocal

  3. 值是怎么传递的?

    • 子线程创建时,JVM 把父线程的inheritableThreadLocals复制给子线程。

九、ThreadLocal中的存储

1. 一个ThreadLocal对象,只能对应一个值

比如:

ThreadLocal<String> threadLocal = new ThreadLocal<>(); threadLocal.set("张三"); threadLocal.set("李四"); System.out.println(threadLocal.get());

结果:

李四

因为:

threadLocal.set("张三");

相当于:

这个 ThreadLocal → "张三"

后来:

threadLocal.set("李四");

就变成:

这个 ThreadLocal → "李四"

所以后面的set()会覆盖前面的值


2.但是一个线程中,可以有很多个ThreadLocal

例如:

ThreadLocal<String> name = new ThreadLocal<>(); ThreadLocal<Integer> age = new ThreadLocal<>(); name.set("张三"); age.set(18); System.out.println(name.get()); System.out.println(age.get());

结果:

张三 18

可以理解成:

当前线程 │ ▼ ThreadLocalMap │ ├── ThreadLocal① → "张三" │ └── ThreadLocal② → 18

所以不是说一个线程只能存一个值

而是:

一个 ThreadLocal 对象只能对应一个值;一个线程可以有很多个 ThreadLocal,因此可以存很多个值。


3. 如果你想让一个 ThreadLocal 存多个数据,也可以

比如:

ThreadLocal<List<String>> threadLocal = new ThreadLocal<>(); List<String> list = new ArrayList<>(); list.add("张三"); list.add("李四"); threadLocal.set(list);

这时候实际上是:

ThreadLocal ↓ List ┌──────┬──────┐ │ 张三 │ 李四 │ └──────┴──────┘

所以从 Java 类型上来说,ThreadLocal<T>T 可以是任何对象,包括ListMap、自定义对象。

你现在可以记住一个非常重要的关系:

一个线程 ↓ ThreadLocalMap ↓ 多个 ThreadLocal ↓ 每个 ThreadLocal 对应一个 Value

也就是:

ThreadLocal 是“键”,Value 是“值”。

这也是为什么源码里会有ThreadLocalMap,而不是简单的一个变量。

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

相关文章:

  • 深入解析唐山网站建设zzvg的核心逻辑与本地化生存之道
  • Forking-Sequences:解决多步预测误差累积与计算效率难题
  • 城市轨道交通时刻表优化:从业务逻辑到数学建模的工程实践
  • 原创角色AI化实践指南:从设定投喂到可控协作的完整方法论
  • 从VC-TURBO看动态资源调度:如何用可变思维优化系统架构
  • 空调遥控器应用案例:ANYCON智能控制解决方案
  • 复指数信号e^jωt:从旋转箭头到信号处理核心的深度解析
  • 快鲸_【v2.1.0】_云商店-智慧园区云平台是什么?
  • 大语言模型指令微调中的局部句法复用:原理、影响与工程应对策略
  • 穿透SEO迷雾:如何甄别Rust技术项目的真实口碑与价值
  • Python文件操作全解析:从原理到实战避坑指南
  • 展厅升级必备,展厅中央控制系统的应用与优势
  • 微信小程序Canvas截图实战:一分钟解决图片生成与保存难题
  • 讲PPT讲得嗓子冒烟?我让AI数字人替我上了,结果…
  • JetBrains Rider 2026.1.1 生成WPF设计页面
  • NPO与CPO技术对比:近封装光学的原理、优势与应用场景
  • MelonLoader终极指南:如何为Unity游戏构建通用模组加载器
  • rust for begginers
  • 异星工厂xp联机总失败?生存建造玩家的自救清单
  • mac终端文件和文件夹操作命令集合
  • SOLO工作者的效率革命:从工具链到心流管理的实战指南
  • 海淀区创业扶持机构哪家入驻流程服务省心:【博亚信诚】高效代办 - 秋山寄远
  • UE5 3DGS 插件:LCC Plugin for UE
  • Claude Code高阶用法:10个提升AI编程效率的实用技巧
  • 本地语音输入法实践指南:从环境配置到工作流集成
  • 游戏手感优化工具:原理、部署与效果验证指南
  • TinyPXE实战:从原理到部署,一站式解决无盘启动与网络安装难题
  • 锐捷交换机十大核心查看命令实战指南:从入门到精通
  • PCB同轴过孔新工艺、SI性能优势及应用前景总结
  • 2026年北京空调维修指南|简单到家服务优势深度解读 - 简单到家