java中ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal三者区别

本文主要介绍 ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal 三者之间区别、如何使用、什么场景使用以及对原理和源码的介绍。介绍原理的时候通过最直白、最易懂的语言争取让大家了解三者之间的区别,以及日常如何把他们使用起来

ThreadLocal

ThreadLocal 解决的是每个线程可以拥有自己线程的变量实例。可以从隔离的角度解决变量线程安全的问题。

举个例子:

用户登陆后将用户的信息保存到 ThreadLocal 中,ThreadLocal<User> 可以保存请求过来的信息。也就是下面在这一个线程中任何地方都可以访问到这个 ThreadLocal 中的变量。虽然这是一个全局的静态变量,但是当有多个个线程调用 UserContext.setUser() 方法的时候,多个线程的变量都会保存,多个线程之间不会被相互覆盖。
看下下面代码,在同一个 JVM 进程中,虽然只有一个静态变量 userHolder,但是
线程A调用:UserContext.setUser(userA)
同时线程B调用:UserContext.setUser(userB)
在线程A中调用 UserContext.getUser() 得到的结果是 userA
在线程B中调用 UserContext.getUser() 得到的结果是 userB

public class UserContext {
    private UserContext() {
    }
    private static ThreadLocal<User> userHolder = new ThreadLocal<>();
    public static User getUser() {
        return userHolder.get();
    }
    public static void setUser(User user) {
        userHolder.set(user);
    }
    public static void clean() {
        userHolder.remove();
    }
}

实现原理:
ThreadLocal 是每个 Thread 都绑定一个 Map,线程之间不会互相干扰
这个Map 不是普通的 Map而是一个定制 Map:ThreadLocalMap。
这个Map 使用了使用“开放寻址法”中最简单的“线性探测法”解决散列冲突问题。这点是和我们平常使用的普通的 HashMap 不一样的。其中还是用了一个神奇的数字 HASH_INCREMENT= 0x61c88647,保证了一个完美的散列分布。具体可以参考斐波那契散列法的相关资料
我们看它的get源码:

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

从上面get源码的前两行可以看到,ThreadLocal的缺点:它不支持子线程。因为map是绑定在currentThread中的。子线程和父线程并不是一个Thread所以产生了ThreadLocal的进化版本

InheritableThreadLocal

前面说到ThreadLocal并不支持子线程,InheritableThreadLocal就是支持子线程的ThreadLocal

举个例子

public class UserContext {
    private UserContext() {
    }
    private static InheritableThreadLocal<User> userHolder = new InheritableThreadLocal<>();
    public static User getUser() {
        return userHolder.get();
    }
    public static void setUser(User user) {
        userHolder.set(user);
    }
    public static void clean() {
        userHolder.remove();
    }
}


@Test
    public void test3() throws InterruptedException {
        UserEntity userEntity = new UserEntity();
        userEntity.setUsername("mzt");
        UserContext.setUser(userEntity);

        UserEntity user = UserContext.getUser();
        Assert.assertNotNull(user);
        Assert.assertEquals(user.getUsername(), "mzt");
        new Thread(() -> {
            final UserEntity user1 = UserContext.getUser();
            Assert.assertNotNull(user1);
            Assert.assertEquals(user1.getUsername(), "mzt");
        }).start();
        TimeUnit.MINUTES.sleep(1);
    }

因为使用了InheritableThreadLocal这时候两个Assert都是正确的。但是仅仅使用ThreadLocal的时候上面例子中new Threa的run方法中getUser的返回值是为null的。如果把InheritableThreadLocal替换为ThreadLocal,那么new Thread中的
UserContext.getUser();返回值是NULL

原理

我们看 InheritableThreadLocal 的源码发现它并没有太多的方法,其实主要的代码是在 Thread 的 init() 方法中。

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

可以看到 inheritableThreadLocals 还是一个 ThreadLocalMap,只不过是在 Thread 的 init 方法中把父Thread的inheritableThreadLocals 变量 copy 了一份给自己。同样借助 ThreadLocalMap 子线程可以获取到父线程的所有变量。
根据它的实现,我们也可以看到它的缺点,就是 Thread的 init 方法是在线程构造方法中 copy的,也就是在线程复用的线程池中是没有办法使用的。

TransmittableThreadLocal

上面介绍了 ThreadLocal 可以同线程共享变量。InheritableThreadLocal 可以父子线程共享变量,那么我们经常使用的线程池如何使用 ThreadLocal 这样的功能呢?TransmittableThreadLocal 简称(TTL)是阿里开源的一款支持线程池的 ThreadLocal 组件。

举个例子:

TenantContext 的实现是使用的 InheritableThreadLocal

public class TenantContext {
    private static InheritableThreadLocal<String> tenantHolder = new InheritableThreadLocal<>();
// 其他方法略....
}
 @Test
    public void test1() throws InterruptedException {
        ExecutorService ttlExecutorService = Executors.newFixedThreadPool(1);
        TenantContext.setTenantId("mzt_" + 1);
        ttlExecutorService.submit(() -> {
            String tenantId = TenantContext.getTenantId();
            log.info("#########, {}", tenantId);
            //TenantContext.clean();
        });
        TimeUnit.SECONDS.sleep(2);
        TenantContext.setTenantId("mzt_" + 2);
        ttlExecutorService.submit(() -> {
            log.info("#########, {}", TenantContext.getTenantId());
           // TenantContext.clean();
        });

        Thread.sleep(2000L);
    }

上面的例子中两次输出都是什么呢?

2021/01/24 21:49:24.678 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:77) #########, mzt_1
2021/01/24 21:49:26.671 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:83) #########, mzt_1

原因如下:例子中使用了固定线程数量为1的固定线程池。第一次sumbit task的时候,线程池会创建线程,因为使用了 InheritableThreadLocal ,所以调用 Thread 的 init 方法的时候会取父线程的 inheritableThreadLocals. 所以第一打印可以打印出 #########, mzt_1。下面又使用了 TenantContext.setTenantId("mzt_" + 2); 方法,之后又 submit 了一个线程,但是这时候线程池里面已经又线程了所以不会重新create线程了,也不会调用 thread 的 init 方法了,所以 get 出来的变量还是 mzt1 。
那么下面改成阿里的TTL

public class TenantContext {
    private static TransmittableThreadLocal<String> tenantHolder = new TransmittableThreadLocal<>();
// 其他方法略....
}

@Test
    public void test1() throws InterruptedException {
        ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
        TenantContext.setTenantId("mzt_" + 1);
        ttlExecutorService.submit(() -> {
            String tenantId = TenantContext.getTenantId();
            log.info("#########, {}", tenantId);
            //TenantContext.clean();
        });
        TimeUnit.SECONDS.sleep(2);
        TenantContext.setTenantId("mzt_" + 2);
        ttlExecutorService.submit(() -> {
            log.info("#########, {}", TenantContext.getTenantId());
           // TenantContext.clean();
        });

        Thread.sleep(2000L);
    }

重新运行之后的结果:

2021/01/24 21:58:58.751 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:77) #########, mzt_1
2021/01/24 21:59:00.746 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:83) #########, mzt_2

原理

我们看到普通的线程池子被 TtlExecutors.getTtlExecutorService() 包裹了一下。如果仅仅是吧InheritableThreadLocal 修改为 TransmittableThreadLocal 是不起作用的。
所以 TTL 的做法也比较直接,使用了装饰器模式,既然 InheritableThreadLocal 只是在线程Create 的时候复制一份父线程数据,那么为了支持线程池就需要在 Thread 的 run 方法之前把父线程的数据 copy 一下就可以了。从源码中看是

 public static ExecutorService getTtlExecutorService(ExecutorService executorService) {
        if (executorService == null || executorService instanceof ExecutorServiceTtlWrapper) {
            return executorService;
        }
        return new ExecutorServiceTtlWrapper(executorService);
    }

重点:ExecutorServiceTtlWrapper包装了我们普通的 ExecutorService,然后充血了 submit 方法

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

重点就是 TtlRunnable 类了,它实现了 Runable 方法,并且重写了 run 方法

 /**
     * wrap method {@link Runnable#run()}.
     */
    @Override
    public void run() {
        Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
        if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }

        Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
        try {
            runnable.run();
        } finally {
            TransmittableThreadLocal.restoreBackup(backup);
        }
    }

这个代码就是流程的核心了,就是在run方法之前复制了父线程的ThreadLocal变量。当线程执行时,调用 TtlRunnable run 方法,TtlRunnable 会从 AtomicReference 中获取出调用线程中所有的上下文,并把上下文给 TransmittableThreadLocal.Transmitter.replay 方法把上下文复制到当前线程。并把上下文备份。
当线程执行完,调用 TransmittableThreadLocal.Transmitter.restore 并把备份的上下文传入,恢复备份的上下文,把后面新增的上下文删除,并重新把上下文复制到当前线程。

延伸

我们发现通过对线程池包裹一层还是侵入性太强,不符合SOLID原则,不优雅。TTL也提供了通过agent方式接入的方法,具体可以在TTL官网看文档。这里就不介绍了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。