InheritableThreadLocal
普通 ThreadLocal 的值,子线程看不到。
但有时候,你确实想让子线程继承父线程的值——比如主线程设置了 TraceId,希望所有子线程都能获取到。
这就是 InheritableThreadLocal 存在的意义。
ThreadLocal vs InheritableThreadLocal
| 特性 | ThreadLocal | InheritableThreadLocal |
|---|---|---|
| 存储位置 | Thread.threadLocals | Thread.inheritableThreadLocals |
| 继承机制 | 无 | 子线程创建时复制父线程的值 |
| 适用场景 | 线程独享数据 | 需要跨线程传递数据 |
父线程: 子线程:
┌──────────────────┐ ┌──────────────────┐
│ ThreadLocalMap │ │ ThreadLocalMap │
│ (threadLocals) │ 创建时复制 ──► │ (inheritable...)│
│ │ │ │
│ userId: "001" │ │ userId: "001" │
└──────────────────┘ └──────────────────┘
值被复制基础用法
java
public class ITLBasicDemo {
private static final InheritableThreadLocal<String> inheritable =
new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 父线程设置值
inheritable.set("父线程的值");
System.out.println("父线程: " + inheritable.get());
// 子线程继承父线程的值
Thread child = new Thread(() ->
System.out.println("子线程继承的值: " + inheritable.get())
);
child.start();
child.join();
}
}输出:
父线程: 父线程的值
子线程继承的值: 父线程的值继承是「快照」,不是「引用同步」
继承发生在子线程创建时,而不是父线程修改值时。
java
public class ITLSnapshotDemo {
private static final InheritableThreadLocal<Integer> count =
new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
count.set(100);
System.out.println("父线程设置: " + count.get());
Thread child = new Thread(() ->
System.out.println("子线程继承的快照: " + count.get()) // 100
);
child.start();
child.join();
// 父线程修改后,子线程不受影响(因为是快照)
count.set(200);
System.out.println("父线程修改后: " + count.get());
}
}子线程在创建时就复制了当时的值,之后父线程再怎么改,子线程都看不到。
父子线程时序问题
这有一个容易踩的坑——如果父线程在创建子线程之后、启动之前修改了值,子线程继承的是哪个版本?
java
public class ITLTimingDemo {
private static final InheritableThreadLocal<String> data =
new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
data.set("主线程数据-V1");
// 方式1:创建后立即启动
Thread child1 = new Thread(() ->
System.out.println("Child1: " + data.get()) // V1
);
child1.start();
// 方式2:先修改,再启动
data.set("主线程数据-V2");
Thread child2 = new Thread(() ->
System.out.println("Child2: " + data.get()) // V2
);
child2.start();
// 方式3:先启动,再修改(Child3 会看到 V1)
Thread child3 = new Thread(() ->
System.out.println("Child3: " + data.get()) // V1(启动时复制)
);
child3.start();
child3.join();
data.set("主线程数据-V3"); // Child3 已经继承了,看不到 V3
child1.join();
child2.join();
}
}关键点:继承发生在 Thread.start() 时,而不是 new Thread() 时。
线程池的陷阱
这是 InheritableThreadLocal 最容易出问题的地方。
java
public class ITLThreadPoolIssue {
private static final InheritableThreadLocal<String> userId =
new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
userId.set("user-001");
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交任务到线程池
executor.submit(() -> {
// ⚠️ 线程池中的线程是复用预先创建的
// 不是在 submit() 时创建,不会继承 submit() 时的值
System.out.println("线程池线程获取: " + userId.get()); // null
});
Thread.sleep(1000);
executor.shutdown();
}
}原因:线程池中的线程是在创建线程池时就预先生成了,它们继承的是创建时的值(此时可能还没有设置 userId)。之后的任务提交不会触发继承。
解决方案一:使用 TransmittableThreadLocal(阿里开源库)
java
// 需要引入 transmittable-thread-local 依赖
import com.alibaba.ttl.TtlRunnable;
public class TTLDemo {
private static final TransmittableThreadLocal<String> context =
new TransmittableThreadLocal<>();
public static void main(String[] args) {
context.set("user-001");
ExecutorService executor = Executors.newFixedThreadPool(2);
// 使用 TtlRunnable 包装
Runnable task = () ->
System.out.println("获取: " + context.get()); // "user-001"
executor.submit(TtlRunnable.get(task));
executor.shutdown();
}
}TransmittableThreadLocal 通过在 Runnable 包装层面传递上下文,解决了线程池复用的问题。
解决方案二:在每次任务执行时手动传递
java
public class ManualPassDemo {
private static final InheritableThreadLocal<String> context =
new InheritableThreadLocal<>();
public static void main(String[] args) {
context.set("user-001");
ExecutorService executor = Executors.newFixedThreadPool(2);
// 在任务内部使用 ThreadLocal
executor.submit(() -> {
String value = context.get(); // 仍然是 null
// 需要其他方式传递
});
executor.shutdown();
}
}实际应用:TraceId 链路追踪
InheritableThreadLocal 最典型的应用场景:分布式链路追踪。
java
public class TraceIdDemo {
private static final InheritableThreadLocal<String> traceId =
new InheritableThreadLocal<>();
public static String getTraceId() {
return traceId.get();
}
public static void setTraceId(String traceIdValue) {
traceId.set(traceIdValue);
}
public static void clear() {
traceId.remove();
}
public static void main(String[] args) {
// HTTP 入口:生成 TraceId
String traceId = generateTraceId();
setTraceId(traceId);
System.out.println("入口 TraceId: " + getTraceId());
// 整个调用链都能获取
processRequest();
}
private static String generateTraceId() {
return "trace-" + UUID.randomUUID().toString().substring(0, 8);
}
private static void processRequest() {
System.out.println("ServiceA - TraceId: " + getTraceId());
callDatabase();
callCache();
}
private static void callDatabase() {
// DAO 层
System.out.println("DAO - TraceId: " + getTraceId());
}
private static void callCache() {
// Cache 层
System.out.println("Cache - TraceId: " + getTraceId());
}
}在传统的同步调用中(new Thread() 创建子线程),这能正常工作。但一旦引入线程池,就需要升级到 TransmittableThreadLocal。
总结对比
| 场景 | ThreadLocal | InheritableThreadLocal | TransmittableThreadLocal |
|---|---|---|---|
| 普通线程创建 | ❌ 不继承 | ✅ 继承 | ✅ 继承 |
| 线程池提交任务 | ❌ 不继承 | ❌ 不继承 | ✅ 继承 |
| ForkJoinPool | ❌ 不继承 | ❌ 不继承 | ✅ 继承 |
| CompletableFuture | ❌ 不继承 | ❌ 不继承 | ✅ 继承 |
| 依赖 | JDK | JDK | 阿里 transmittable-thread-local |
注意事项
- 继承时机:值在子线程创建时复制(
start()时),不是访问时 - 线程池问题:InheritableThreadLocal 在线程池场景下不可靠,使用 TransmittableThreadLocal
- 性能开销:比 ThreadLocal 多一次数据复制的开销
- 内存泄漏:同样需要
remove()清理
要点回顾
- InheritableThreadLocal 让子线程继承父线程的值
- 继承是「快照」,发生在
start()时,不是new时 - 线程池场景下不可靠(线程复用不触发继承)
- 生产环境推荐使用
TransmittableThreadLocal(阿里开源) - 同样需要
remove()防止内存泄漏
相关链接
- ThreadLocal 核心概念 - 基本原理
- ThreadLocal 方法详解 - API 使用
- ThreadLocal 内存泄漏 - 清理机制
