图解Java线程间本地变量传递

664 阅读11分钟

前言

Java编程中,常常使用ThreadLocal来设置线程本地变量,通过ThreadLocal设置的本地变量,在同一线程的其它地方,都可以通过ThreadLocal方便的获取到,但是ThreadLocal设置的本地变量,无法跨线程传递,这种本地变量需要跨线程的场景,更适合使用InheritableThreadLocal,即通过InheritableThreadLocal设置的本地变量,可以传递给子线程,但是这里的传递是有条件,即只能传递给在当前线程中new出来的Thread,这就极大的限制了InheritableThreadLocal的使用,因为我们通常是不会直接new一个Thread来使用的,而是会使用线程池,线程池里的线程通常就不是在当前线程new出来的,所以此时就需要使用阿里开源的TransmittableThreadLocal来真正的完成线程本地变量的传递。

ThreadLocalInheritableThreadLocalTransmittableThreadLocal的源码分享,其实基本是一搜一大堆的文章,但是我个人觉得这三者,首先需要很熟练的使用,其次了解其原理就可以了,不需要去看源码,因为这三者的源码,有时候真的很抽象,懂的自然懂,不懂的会看得很头痛,所以本文绝对不会引入任何源码,以案例结合图文的形式,完全搞定Java线程间是如何实现本地变量的传递的。

transmittable-thread-local版本:2.11.4

正文

一. ThreadLocal

一切的一切,都是建立在ThreadLocal这个好东西上的,所以就算已经对ThreadLocal滚瓜烂熟了,也还是要从ThreadLocal开始分析,关于ThreadLocal更加深入的源码分析,感兴趣的可以去看看详解ThreadLocal,在这里就不会再提到任何关于源码的东西了。

1. 使用案例

先看案例。

@Test
public void 简单使用ThreadLocal() throws Exception {
    ThreadLocal<String> threadLocal_A = new ThreadLocal<>();
    ThreadLocal<String> threadLocal_B = new ThreadLocal<>();
    CountDownLatch countDownLatch = new CountDownLatch(2);

    new Thread(new Runnable() {
        @Override
        public void run() {
            threadLocal_A.set("aaa");

            System.out.println("线程:" + Thread.currentThread().getName()
                    + "从threadLocal_A中获取数据为"
                    + threadLocal_A.get());

            threadLocal_A.remove();
            countDownLatch.countDown();
        }
    }, "Thread-1").start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            threadLocal_A.set("bbb");
            threadLocal_B.set("ccc");

            System.out.println("线程:" + Thread.currentThread().getName()
                    + "从threadLocal_A中获取数据为"
                    + threadLocal_A.get());
            System.out.println("线程:" + Thread.currentThread().getName()
                    + "从threadLocal_B中获取数据为"
                    + threadLocal_B.get());

            threadLocal_A.remove();
            threadLocal_B.remove();
            countDownLatch.countDown();
        }
    }, "Thread-2").start();

    countDownLatch.await();
}

运行单元测试,打印如下。

线程:Thread-1从threadLocal_A中获取数据为aaa
线程:Thread-2从threadLocal_A中获取数据为bbb
线程:Thread-2从threadLocal_B中获取数据为ccc

那么简单来看,在同一个线程中,通过同一个ThreadLocal对象set一个值,那么在同一个线程中的其它地方,就能通过同一个ThreadLocal对象get到之前set的值,ThreadLocal的使用就这么点东西。

2. 图解ThreadLocal

ThreadLocal通过set() 设置值时,其实就是把自己当作key,然后把要设置的值当作value,存放在了当前线程对应的Thread对象的threadLocals字段中,这个threadLocals字段,其实就是一个Map,后续get() 值时,其实就是从threadLocals这个Map中通过ThreadLocal对象这个key把对应的value获取出来。

如果上面的说法有点抽象,那么请看下面的图解。

并发编程-ThreadLocal图解

有两个ThreadLocal,分别为threadLocal_AthreadLocal_BThread-1中使用threadLocal_A设置了值为aaaThread-2中使用threadLocal_A设置了值为bbb,使用threadLocal_B设置了值为ccc,那么此时两个线程中的threadLocals就长成上图这样了。

那么相应的,此时在Thread-1中,只能通过threadLocal_A获取到aaa,在Thread-2中,可以通过threadLocal_A获取到bbb,也可以通过threadLocal_B获取到ccc

二. InheritableThreadLocal

ThreadLocal说到底其实就是线程内部自己玩,而InheritableThreadLocal可以做到线程间一起玩。

1. 使用案例

直接看案例。

@Test
public void 简单使用InheritableThreadLocal() throws Exception {
    ThreadLocal<String> threadLocal_A = new ThreadLocal<>();
    ThreadLocal<String> threadLocal_B = new ThreadLocal<>();
    ThreadLocal<String> inheritableThreadLocal_C = new InheritableThreadLocal<>();

    CountDownLatch countDownLatch = new CountDownLatch(2);

    new Thread(new Runnable() {
        @Override
        public void run() {
            threadLocal_A.set("aaa");
            threadLocal_B.set("bbb");
            inheritableThreadLocal_C.set("ccc");
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程:" + Thread.currentThread().getName()
                            + "从threadLocal_A中获取数据为"
                            + threadLocal_A.get());
                    System.out.println("线程:" + Thread.currentThread().getName()
                            + "从threadLocal_B中获取数据为"
                            + threadLocal_B.get());
                    System.out.println("线程:" + Thread.currentThread().getName()
                            + "从inheritableThreadLocal_C中获取数据为"
                            + inheritableThreadLocal_C.get());

                    threadLocal_A.remove();
                    threadLocal_B.remove();
                    inheritableThreadLocal_C.remove();
                    countDownLatch.countDown();
                }
            }, "Thread-2").start();
            countDownLatch.countDown();
        }
    }, "Thread-1").start();

    countDownLatch.await();
}

运行单元测试,打印如下。

线程:Thread-2从threadLocal_A中获取数据为null
线程:Thread-2从threadLocal_B中获取数据为null
线程:Thread-2从inheritableThreadLocal_C中获取数据为ccc

InheritableThreadLocalThreadLocal的使用方式其实是一样的,set() 方法设置值,get() 方法获取值,区别就是InheritableThreadLocal在父线程中set的值,在子线程中也能get到,而这一点ThreadLocal做不到。

2. 图解InheritableThreadLocal

线程对应的Thread对象,除了有一个threadLocals字段,还有一个inheritableThreadLocals字段,这两个字段是一模一样的,都是一个Map,其中ThreadLocal对应的键值对放在threadLocals中,而InheritableThreadLocal对应的键值对是放在inheritableThreadLocals中,在父线程中创建子线程时,父线程会把自己的inheritableThreadLocals传递给子线程,而这就正是InheritableThreadLocal设置的本地变量可以从父线程传递到子线程的秘密。

如果感觉有点抽象,那么请看下面的图解。

并发编程-InheritableThreadLocal图解

我们有两个ThreadLocal,分别为threadLocal_AthreadLocal_B,有一个InheritableThreadLocal,为inheritableThreadLocal_CThread-1中使用threadLocal_A设置了值为aaa,使用threadLocal_B设置了值为bbb,使用inheritableThreadLocal_C设置了值为ccc,此时Thread-1中的threadLocalsinheritableThreadLocals就像上图那样。

此时如果在Thread-1中创建Thread-2,在创建Thread-2时,如果Thread-1中的inheritableThreadLocals不为空,则会新创建一个inheritableThreadLocals出来,然后把Thread-1中的inheritableThreadLocals的键值对全部拷贝到新创建的inheritableThreadLocals中。

最终通过threadLocal_AthreadLocal_B都无法在Thread-2中获取到值,但是通过inheritableThreadLocal_CThread-2中可以获取到ccc

三. TransmittableThreadLocal

InheritableThreadLocal可以解决父子线程的本地变量传递的问题,但是大部分时候却无法将本地变量传递到线程池里线程,而TransmittableThreadLocal就解决了这个问题。

1. 使用案例

直接看使用案例。

@Test
public void 线程池下使用TransmittableThreadLocal() throws Exception {
    ThreadLocal<String> transmittableThreadLocal_A = new TransmittableThreadLocal<>();
    ThreadLocal<String> transmittableThreadLocal_B = new TransmittableThreadLocal<>();
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1,
            60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "Pool-Thread-1");
        }
    });

    CountDownLatch countDownLatch = new CountDownLatch(5);

    for (int i = 0; i < 5; i++) {
        transmittableThreadLocal_A.set("aaa" + i);
        transmittableThreadLocal_B.set("bbb" + i);
        threadPoolExecutor.execute(TtlRunnable.get(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程:" + Thread.currentThread().getName()
                        + "从transmittableThreadLocal_A中获取数据为"
                        + transmittableThreadLocal_A.get());
                System.out.println("线程:" + Thread.currentThread().getName()
                        + "从transmittableThreadLocal_B中获取数据为"
                        + transmittableThreadLocal_B.get());
                transmittableThreadLocal_A.remove();
                transmittableThreadLocal_B.remove();
                countDownLatch.countDown();
            }
        }));
    }

    countDownLatch.await();
}

运行单元测试,打印如下。

线程:Pool-Thread-1从transmittableThreadLocal_A中获取数据为aaa0
线程:Pool-Thread-1从transmittableThreadLocal_B中获取数据为bbb0
线程:Pool-Thread-1从transmittableThreadLocal_A中获取数据为aaa1
线程:Pool-Thread-1从transmittableThreadLocal_B中获取数据为bbb1
线程:Pool-Thread-1从transmittableThreadLocal_A中获取数据为aaa2
线程:Pool-Thread-1从transmittableThreadLocal_B中获取数据为bbb2
线程:Pool-Thread-1从transmittableThreadLocal_A中获取数据为aaa3
线程:Pool-Thread-1从transmittableThreadLocal_B中获取数据为bbb3
线程:Pool-Thread-1从transmittableThreadLocal_A中获取数据为aaa4
线程:Pool-Thread-1从transmittableThreadLocal_B中获取数据为bbb4

TransmittableThreadLocal本身的使用,和InheritableThreadLocal以及ThreadLocal的使用方式其实是一样的,就是set() 方法设置值,然后通过get() 方法获取值,但是有一点不同的是,此时线程池运行的任务不再是Runnable,而是TtlRunnable,至于为什么,后面图解见分晓。

2. 图解TransmittableThreadLocal

我们先思考一下,为什么InheritableThreadLocal在线程池的场景下不好使了,其实就是因为InheritableThreadLocal只能作用于父子线程的场景,而使用线程池时,线程池里面的线程的创建,是有一套机制在里面的,在大部分时候,我们的业务线程和线程池里面的线程,都不构成父子关系,那么InheritableThreadLocal自然就不好使了。

那么现在换做是你,你会怎么设计来解决这个问题。仔细想一想,我们的业务线程,以及线程池里的线程,它们之间的唯一纽带是什么,毫无疑问是任务Runnable,我们在业务线程中创建任务Runnable,然后丢到线程池里,后续线程池里的线程拿到任务Runnable,调用其run() 方法完成执行,所以如果让我们来设计解决业务线程的本地变量无法传递给线程池线程这个问题的话,我们就需要在任务Runnable上面做文章,一个很简单的思路就是:创建Runnable的时候,把当前业务线程的本地变量放到Runnable中,在线程池的线程拿到Runnable准备执行前,把业务线程存放到Runnable中的本地变量拿出来并设置到线程池的线程中,这样就完成了业务线程的本地变量到线程池线程的传递。很荣幸,TransmittableThreadLocal也是这么做的。

先看一下TransmittableThreadLocal调用set() 方法时会发生什么,图解如下。

并发编程-TransmittableThreadLocal-set图解

因为TransmittableThreadLocal继承于InheritableThreadLocal,所以TransmittableThreadLocal调用set() 方法时会将set的值先存放一份到Thread-1inheritableThreadLocals中,然后就是最关键的一步,调用了set() 方法的TransmittableThreadLocal会将自己存放在一个WeakHashMap中,而这个WeakHashMap是一个叫做holderInheritableThreadLocal设置到inheritableThreadLocals中的,这么说起来有点绕,不过你看看上面的图,其实就比较清楚了,简而言之,这个holder,可以为线程hold住所有在线程里面设置过值的TransmittableThreadLocal,我们通过holder就可以拿到这些TransmittableThreadLocal

之前我们已经明确,要把本地变量放到Runnable中来传递给线程池里的线程,那么当前已有的Runnable是无法满足这个需求的,所以这里需要使用TtlRunnable,我们在把真正的Runnable丢给线程池前,需要先将Runnable创建为TtlRunnable,创建出来的TtlRunnable最终会持有一个叫做ttl2Value的字段,该字段是一个Map,键是holderThread-1线程hold住的所有的TransmittableThreadLocal,例如transmittableThreadLocal-A,值就是transmittableThreadLocal-A设置在Thread-1中的值aaa,就像下面这样。

并发编程-TtlRunnable结构图

看完上图,TtlRunnable长啥样就一目了然,那么ttl2Value是怎么得到的呢,首先要知道,创建TtlRunnable时,我们还是在Thread-1中,所以可以通过holder拿到存放在Thread-1中的WeakHashMap,然后拿到WeakHashMap的所有键,就拿到了transmittableThreadLocal-AtransmittableThreadLocal-B,此时再调用transmittableThreadLocal-AtransmittableThreadLocal-Bget() 方法,就拿到aaabbb,那么以transmittableThreadLocal-AtransmittableThreadLocal-B为键,aaabbb为值,放到一个叫做ttl2ValueMap里,就得到ttl2Value了。

所以我们在Thread-1中创建TtlRunnable时,就完成了将本地变量从Thread-1转移到了ttlRunnable-1中,具体就像下面展示的这样。

并发编程-本地变量转移到TtlRunnable示意图

假如线程池里面的Pool-Thread-1线程拉取到了ttlRunnable-1,此时就会调用到ttlRunnable-1run() 方法,在run() 方法中就会遍历ttl2Value的每一个键值对,调用作为键的TransmittableThreadLocalset() 方法,把值设置到Pool-Thread-1中,最终ttl2Value的键值对就转移到了Pool-Thread-1inheritableThreadLocals中,就像下图这样。

并发编程-TtlRunnable-run图解

最终通过TransmittableThreadLocal保存在Thread-1中的本地变量,借助TtlRunnable传递给了线程池的线程Pool-Thread-1,那么在Pool-Thread-1中,通过transmittableThreadLocal-A可以获取到aaa,通过transmittableThreadLocal-B可以获取到bbb

总结

要跨线程传递本地变量,在父子线程场景下,可以使用InheritableThreadLocal,作用的原理简单概述就是在父线程中创建子线程时,父线程会把通过InheritableThreadLocal设置的本地变量给到子线程,那么在子线程中就可以获取到这些本地变量了,但是在线程池的场景下,InheritableThreadLocal不再适用,这是因为业务线程和线程池里面的线程,几乎都不构成父子关系,所以InheritableThreadLocal不好使,此时应该使用TransmittableThreadLocal,但是TransmittableThreadLocal不能单独使用,需要配合TtlRunnable一起使用,我们的Runnable在丢到线程池之前,需要先封装为TtlRunnable再丢进去,这时在业务线程中通过TransmittableThreadLocal设置的本地变量,也能传递给运行TtlRunnable的线程池线程,作用的原理简单概述就是业务线程中通过TransmittableThreadLocal设置的本地变量先传递给了TtlRunnable,然后再通过TtlRunnable传递给了运行TtlRunnable的线程池线程。