ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal 区别与使用

1,110 阅读2分钟

ThreadLocal

ThreadLocal解决的是每个线程可以拥有自己线程的变量实例。可以从隔离的角度解决变量线程安全,相信大家也十分熟悉,此处不再演示。

但是它并不支持子线程,因为父线程与子线程并不是同一个Thread,例:

public class UserContext {
    private static ThreadLocal<String> userHolder = new ThreadLocal<>();

    public static String getUser() {
        return userHolder.get();
    }

    public static void setUser(String user) {
        userHolder.set(user);
    }

    public static void clean() {
        userHolder.remove();
    }
}
public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        UserContext.setUser("小明");

        System.out.println("父线程获取:" + UserContext.getUser());
        new Thread(() -> {
            // 无法获取父线程的ThreadLocal
            System.out.println("子线程获取:" + UserContext.getUser());
        }).start();


        Thread.sleep(1000);
    }
}

JDK为此提供另一个类解决此问题 ↓

InheritableThreadLocal

我们只需将上面的演示代码 new ThreadLocal<>() 替换成 new InheritableThreadLocal<>() 即可解决问题。

原理: 点开InheritableThreadLocal,实没多少代码,原理的码在此,java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)

init方法末尾初:

...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

可以看到inheritableThreadLocals还是一个ThreadLocalMap,只不过是在Thread的init方法中把父Thread的inheritableThreadLocals变量copy了一份给自己。同样借助ThreadLocalMap子线程可以获取到父线程的所有变量。

根据它的实现,我们也可以看到它的缺点,就是Thread的init方法是在线程构造方法中copy的;

也就是说inheritThreadLocals 一旦创建就会copy父线程的信息,但是在线程池这种复用线程的场景下,线程被多次复用,而inheritThreadLocals还是原来的,这样就会有问题了。如下

public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        ExecutorService ttlExecutorService = Executors.newFixedThreadPool(1);
        UserContext.setUser("小明");
        
        ttlExecutorService.submit(() -> {
            System.out.println("第一次,子线程获取:" + UserContext.getUser());
        });
        
        UserContext.setUser("小红");
        ttlExecutorService.submit(() -> {
            //此处由于是复用线程,所以获得的用户依然是小明
            System.out.println("第二次,子线程获取:" + UserContext.getUser());
        });

        Thread.sleep(1000);
    }
}

为此阿里开源了一个工具 TransmittableThreadLocal

TransmittableThreadLocal

使用例子:

public class UserContext {
    private static ThreadLocal<String> userHolder = new TransmittableThreadLocal<>();

    public static String getUser() {
        return userHolder.get();
    }

    public static void setUser(String user) {
        userHolder.set(user);
    }

    public static void clean() {
        userHolder.remove();
    }
}
public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        //需要手动修饰线程池
        //如果仅仅是吧InheritableThreadLocal修改为TransmittableThreadLocal是不起作用的。
        executorService = TtlExecutors.getTtlExecutorService(executorService);


        UserContext.setUser("小明");

        executorService.submit(() -> {
            System.out.println("第一次,子线程获取:" + UserContext.getUser());
        });

        UserContext.setUser("小红");
        executorService.submit(() -> {
            //不再是小明,而是小红
            System.out.println("第二次,子线程获取:" + UserContext.getUser());
        });
        executorService.shutdown();
        Thread.sleep(1000);
    }
}

原理:在线程run方法之前复制了父线程的ThreadLocal变量

先看修饰线程池的代码做了啥

@Nullable
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
    return (ExecutorService)(!TtlAgent.isTtlAgentLoaded() && executorService != null && !(executorService instanceof TtlEnhanced) ? new ExecutorServiceTtlWrapper(executorService, true) : executorService);
}

主要看ExecutorServiceTtlWrapper里面的内容

提交任务时,使用TtlRunnable创建方法

@NonNull
@Override
public <T> Future<T> submit(@NonNull Runnable task, T result) {
    return executorService.submit(TtlRunnable.get(task, false, idempotent), result);
}

TtlRunnable 在run方法之前复制了父线程的ThreadLocal变量 com.alibaba.ttl.TtlRunnable#run

@Override
public void run() {
    //当前线程ThreadLocal
    final Object captured = capturedRef.get();
    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
    //父线程ThreadLocal
    final Object backup = replay(captured);
    try {
        runnable.run();
    } finally {
        restore(backup);
    }
}

它还提供利用Java Agent,无需修饰线程池的无入侵用法 请查阅 TransmittableThreadLocal