ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal三者之间区别

目录

    • ThreadLocal
        • 举个例子
        • 原理
    • InheritableThreadLocal
        • 举个例子
        • 原理
    • TransmittableThreadLocal
        • 举个例子
        • 原理
          • 延伸
      • 参考:

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

ThreadLocal

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

举个例子

用户登陆后将用户的信息保存到ThreadLocal中,ThreadLocal 可以保存请求过来的信息。也就是下面在这一个线程中任何地方都可以访问到这个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 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 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 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 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  Future 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, Object> copied = copiedRef.get();

        if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {

            throw new IllegalStateException("TTL value reference is released after run!");

        }

        Map, 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官网看文档。这里就不介绍了。

参考:

https://www.cnblogs.com/dennyzhangdd/p/7978455.html

https://www.javaspecialists.eu/archive/Issue164-Why-0x61c88647.html

https://blog.xieyangzhe.com/archives/45

你可能感兴趣的:(java面试,java,java)