InheritableThreadLocal 详解

发布时间:2026/8/14 4:23:16
InheritableThreadLocal 详解 你已经知道ThreadLocal可以让每个线程拥有自己的私有变量互不干扰。但如果遇到父子线程一个线程里再开一个新线程的场景ThreadLocal就会暴露一个大坑——子线程拿不到父线程的值。InheritableThreadLocal就是来解决这个问题的。一、ThreadLocal 的痛点父子线程不共享先看一个例子感受一下问题public class ThreadLocalDemo { // 普通的 ThreadLocal private static ThreadLocalString 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换成InheritableThreadLocalpublic class InheritableThreadLocalDemo { // 换成 InheritableThreadLocal private static InheritableThreadLocalString 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 InheritableThreadLocalString 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 InheritableThreadLocalUser 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); InheritableThreadLocalString 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对象只能对应一个值比如ThreadLocalString threadLocal new ThreadLocal(); threadLocal.set(张三); threadLocal.set(李四); System.out.println(threadLocal.get());结果李四因为threadLocal.set(张三);相当于这个 ThreadLocal → 张三后来threadLocal.set(李四);就变成这个 ThreadLocal → 李四所以后面的set()会覆盖前面的值。2.但是一个线程中可以有很多个ThreadLocal例如ThreadLocalString name new ThreadLocal(); ThreadLocalInteger 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 存多个数据也可以比如ThreadLocalListString threadLocal new ThreadLocal(); ListString list new ArrayList(); list.add(张三); list.add(李四); threadLocal.set(list);这时候实际上是ThreadLocal ↓ List ┌──────┬──────┐ │ 张三 │ 李四 │ └──────┴──────┘所以从 Java 类型上来说ThreadLocalT的T 可以是任何对象包括List、Map、自定义对象。你现在可以记住一个非常重要的关系一个线程 ↓ ThreadLocalMap ↓ 多个 ThreadLocal ↓ 每个 ThreadLocal 对应一个 Value也就是ThreadLocal 是“键”Value 是“值”。这也是为什么源码里会有ThreadLocalMap而不是简单的一个变量。