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

前言

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. 使用案例

先看案例。

java 复制代码
@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();
}

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

txt 复制代码
线程: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_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. 使用案例

直接看案例。

java 复制代码
@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();
}

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

txt 复制代码
线程: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设置的本地变量可以从父线程传递到子线程的秘密。

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

我们有两个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. 使用案例

直接看使用案例。

java 复制代码
@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();
}

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

txt 复制代码
线程: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 继承于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 长啥样就一目了然,那么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中,具体就像下面展示的这样。

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

最终通过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的线程池线程。

相关推荐
路在脚下@10 分钟前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
啦啦右一12 分钟前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien13 分钟前
Spring Boot常用注解
java·spring boot·后端
苹果醋31 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
Hello.Reader2 小时前
深入解析 Apache APISIX
java·apache
盛派网络小助手2 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
∝请叫*我简单先生2 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl