Skip to content

InheritableThreadLocal

普通 ThreadLocal 的值,子线程看不到。

但有时候,你确实想让子线程继承父线程的值——比如主线程设置了 TraceId,希望所有子线程都能获取到。

这就是 InheritableThreadLocal 存在的意义。

ThreadLocal vs InheritableThreadLocal

特性ThreadLocalInheritableThreadLocal
存储位置Thread.threadLocalsThread.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

总结对比

场景ThreadLocalInheritableThreadLocalTransmittableThreadLocal
普通线程创建❌ 不继承✅ 继承✅ 继承
线程池提交任务❌ 不继承❌ 不继承✅ 继承
ForkJoinPool❌ 不继承❌ 不继承✅ 继承
CompletableFuture❌ 不继承❌ 不继承✅ 继承
依赖JDKJDK阿里 transmittable-thread-local

注意事项

  1. 继承时机:值在子线程创建时复制(start() 时),不是访问时
  2. 线程池问题:InheritableThreadLocal 在线程池场景下不可靠,使用 TransmittableThreadLocal
  3. 性能开销:比 ThreadLocal 多一次数据复制的开销
  4. 内存泄漏:同样需要 remove() 清理

要点回顾

  • InheritableThreadLocal 让子线程继承父线程的值
  • 继承是「快照」,发生在 start() 时,不是 new
  • 线程池场景下不可靠(线程复用不触发继承)
  • 生产环境推荐使用 TransmittableThreadLocal(阿里开源)
  • 同样需要 remove() 防止内存泄漏

相关链接

基于 VitePress 构建