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 是什么?
InheritableThreadLocal是ThreadLocal的子类。它的特殊之处在于:子线程创建时,会自动把父线程的值复制一份到自己的空间里。
只改一行代码,把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) │ └─────────────────────────────────────────────┘八、面试常问
ThreadLocal 和 InheritableThreadLocal 的区别?
ThreadLocal:线程隔离,父子线程不共享。InheritableThreadLocal:子线程创建时继承父线程的值,之后各自独立。
InheritableThreadLocal 在线程池里能用吗?
不能直接用,因为线程池复用线程,不会触发"创建时复制"。需要用
TransmittableThreadLocal。
值是怎么传递的?
子线程创建时,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 可以是任何对象,包括List、Map、自定义对象。
你现在可以记住一个非常重要的关系:
一个线程 ↓ ThreadLocalMap ↓ 多个 ThreadLocal ↓ 每个 ThreadLocal 对应一个 Value也就是:
ThreadLocal 是“键”,Value 是“值”。
这也是为什么源码里会有ThreadLocalMap,而不是简单的一个变量。
