ThreadLocal 如何实现父子线程共享变量?

416 阅读4分钟

在 Java 中,ThreadLocal 默认仅在当前线程内共享变量,父子线程无法直接共享(子线程会创建独立的 ThreadLocalMap)。实现父子线程共享变量,核心是让子线程能访问到父线程的 ThreadLocal 数据,主流方案有以下 3 种,按推荐度排序:

一、推荐方案:使用 InheritableThreadLocal(JDK 原生支持)

这是 JDK 专门为“父子线程变量继承”设计的类,是 ThreadLocal 的子类,底层通过子线程初始化时复制父线程数据实现共享。

核心原理

1.父线程中,InheritableThreadLocal 会将变量存储到线程的 inheritableThreadLocals 成员变量中(而非 ThreadLocal 用的 threadLocals);

2.当父线程创建子线程(如 new Thread())时,JVM 会触发 Thread 类的初始化逻辑,自动检查父线程是否有 inheritableThreadLocals 数据;

3.若有,子线程会浅拷贝父线程的 inheritableThreadLocals 到自己的 inheritableThreadLocals 中,实现变量继承。

使用示例

// 1. 定义 InheritableThreadLocal
private static final ThreadLocal<String> parentChildShared = new InheritableThreadLocal<>();

public static void main(String[] args) {
    // 2. 父线程设置变量
    parentChildShared.set("父线程的变量");

    // 3. 子线程获取变量(能直接拿到父线程的值)
    new Thread(() -> {
        System.out.println(parentChildShared.get()); // 输出:父线程的变量
        parentChildShared.remove(); // 避免内存泄漏
    }).start();

    parentChildShared.remove(); // 父线程用完也需清理
}

注意点

浅拷贝限制:若存储的是对象(如 User),父子线程共享的是对象引用,一方修改对象内部属性会影响另一方;

线程池不适用:线程池中的线程是复用的,子线程初始化时仅复制一次父线程数据,后续复用线程不会重新复制(需结合方案三解决)。

二、基础方案:手动传递变量(简单场景)

若仅需在“父线程创建子线程时”传递变量,可直接通过子线程的构造函数、方法参数手动传入,无需依赖 ThreadLocal 相关类。

使用示例

public static void main(String[] args) {
    // 父线程定义变量
    String sharedVar = "父线程的变量";

    // 子线程通过构造函数接收变量
    new Thread(new ChildTask(sharedVar)).start();
}

// 子线程任务类,通过成员变量存储共享值

static class ChildTask implements Runnable {
    private String sharedVar;

    // 构造函数接收父线程传递的变量
    public ChildTask(String sharedVar) {
        this.sharedVar = sharedVar;
    }

    @Override
    public void run() {
        System.out.println(sharedVar); // 输出:父线程的变量
    }
}

适用场景

父子线程关系简单,无复杂线程复用(如非线程池场景);

仅需传递少量变量,无需全局共享(避免 ThreadLocal 的内存泄漏风险)。

三、进阶方案:TransmittableThreadLocal(解决线程池复用问题)

当使用线程池(如 ThreadPoolExecutor)时,InheritableThreadLocal 会失效(线程复用导致变量不更新)。此时需使用阿里开源的 TransmittableThreadLocal(TTL),它能在线程池任务执行前,重新将父线程的变量复制到复用的子线程中。

核心原理

1.TransmittableThreadLocal 继承自 InheritableThreadLocal,额外维护了“变量传递上下文”;

2.通过 TtlRunnable/TtlCallable 包装线程池任务,在任务执行前(run() 方法调用前),自动将当前父线程的 TransmittableThreadLocal 数据复制到复用的子线程中;

3.任务执行后,自动清理子线程中的变量,避免影响后续任务。

使用步骤

1.引入依赖(Maven):

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version> <!-- 最新版本需自查 -->
</dependency>

2.代码示例:

// 1. 定义 TransmittableThreadLocal
private static final ThreadLocal<String> ttlShared = new TransmittableThreadLocal<>();

public static void main(String[] args) {
    // 2. 创建线程池(复用线程)
    ExecutorService executor = Executors.newFixedThreadPool(1);

    // 3. 父线程设置变量(每次提交任务前可更新)
    ttlShared.set("父线程的变量(线程池场景)");

    // 4. 用 TtlRunnable 包装任务,确保变量传递
    executor.submit(TtlRunnable.get(() -> {
        System.out.println(ttlShared.get()); // 输出:父线程的变量(线程池场景)
        ttlShared.remove();
    }));

    ttlShared.remove();
    executor.shutdown();
}

适用场景

核心场景:线程池复用线程时,需动态传递父线程的 ThreadLocal 变量(如分布式追踪中的 traceId、用户登录态 userId)。

总结:方案选择

场景推荐方案优点缺点
普通父子线程(非线程池)InheritableThreadLocalJDK 原生,无需额外依赖线程池复用失效,浅拷贝限制
简单传递(少量变量)手动传递(构造函数/参数)无内存泄漏风险,逻辑清晰不支持全局共享,代码侵入性强
线程池(动态传递)TransmittableThreadLocal解决线程复用问题,支持全局共享需引入第三方依赖,需包装任务