从ThreadLocal谈到TransmittableThreadLocal,从使用到原理

3,640 阅读24分钟

本文正在参加「金石计划」

从ThreadLocal到InheritableThreadLocal再到TransmittableThreadLocal,从简单的使用,到明白个别知识原理。

前言

谈到这个其实还是蛮有意思的,因为我最近有在看SpringCloud相关的有趣的知识点,在玩那个链路追踪(sleuth+zipkin)的时候,看博客看着看着,就变成了看关于怎么自己手动实现链路追踪的文章去啦

因为小小的好奇心吧,然后在掘金看到了这篇:自实现分布式链路追踪 方案&实践-作者:蝎子莱莱爱打怪

也就是这篇文章中提到了我今天想谈的这个知识点 TransmittableThreadLocal ,顺着文章提供的链接,我就去了github上溜达。

至此才有了我笔下的这篇东拼西凑的博文~ 虽然学过不少多线程的知识了,但我可以说这是我第一次接触TransmittableThreadLocal 吗 🤕 哈哈

希望能够有一些收获吧


在开始聊 TransmittableThreadLocal 之前,不可避免的还是要先说一说大家相对熟悉的 ThreadLocalInheritableThreadLocal 的。

知道痛点的由来,才能更清楚 TransmittableThreadLocal 的产生以及使用场景。

如果是已经了解过ThreadLocal和InheritableThreadLocal的朋友,可以直接点击TransmittableThreadLocal目录开始阅读。

image.png

ThreadLocal

ThreadLocal 相对来说,大伙应该都是非常熟悉的啦,不然你可能也不会点开这篇博客啦,哈哈

ThreadLocal直接翻译为线程本地(变量),我们经常会使用到它来保存一些线程隔离的全局的变量信息。使用ThreadLocal维护变量时,每个线程都会获得该线程独享一份变量副本。

ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,即变量在线程间隔离而在方法或类间共享的场景。 确切的来说,ThreadLocal 并不是专门为了解决多线程共享变量产生的并发问题而出来的,而是给提供了一个新的思路,曲线救国。

使用场景

简单说一下我看到过的~

  1. 保存用户的登录信息

    在没有使用权限框架的单体项目中,ThreadLocal 可能会用来临时保存请求时的用户信息。

  2. 链路追踪

    当前端发送请求到服务 A时,服务 A会生成一个类似UUIDtraceId字符串,将此字符串放入当前线程的ThreadLocal中,在调用服务 B的时候,将traceId写入到请求的Header中,服务 B在接收请求时会先判断请求的Header中是否有traceId,如果存在则写入自己线程的ThreadLocal中。

总的来说就是上下文信息的传递以及线程隔离的使用场景会比较适合。

注意:ThreadLocal保存的信息只能够在当前线程中可访问到,如果再开一个异步线程则无法进行访问,后续会说。

举个小例子

public class ThreadLocalDemo1 {
​
    private static ThreadLocal<String> userHolder = new ThreadLocal<>();
​
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 保存临时用户信息");
            String userInfo="宁在春";
            userHolder.set(userInfo);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 不会收到线程2的影响,因为ThreadLocal 线程本地存储
            System.out.println(Thread.currentThread().getName() + " 获取临时用户信息 " + userHolder.get());
            // 线程结束前,需要移除
            userHolder.remove();
        }, "myThread1").start();
​
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 保存临时用户信息");
            String userInfo="hello world";
            userHolder.set(userInfo);
            userHolder.remove();
        }, "myThread2").start();
    }
}
#输出
myThread1 保存临时用户信息
myThread2 保存临时用户信息
myThread1 获取临时用户信息 宁在春

从结果可以很明显的看出,线程之间的变量并不相互影响。

类图结构

image.png

图2:关键类图

Thread中有两个变量分别是ThreadLocal.ThreadLocalMap threadLocalsinheritableThreadLocalsinheritableThreadLocals后续再谈。

在这里我们可以知道的是每个线程都会有一个自己的 ThreadLocalMap,而ThreadLocalMap是ThreadLocal下的一个内部类.

ThreadLocalMap从命名上也可以看出来,它就是一个Map结构的对象(不过它不同于HashMap,它没有链表),ThreadLocalMap的key值是ThreadLocal,value则是我们要放入的值。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

ThreadLocalMap中的内部Entry,就是用来保存键值对的,Entry 继承了 WeakReference(弱引用),为防止内存泄漏而设计的。

public class ThreadLocal<T>{
​
    static class ThreadLocalMap {
 
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
​
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

(Java引用相关的知识,大家需要去自己了解一下下)

怎么实现线程隔离的?

要说是怎么实现线程隔离的,其实就是在set()、get()方法的具体实现,我们set的值,为什么不会被其他的线程所读取。

set()方法

public void set(T value) {
    // 1、获取当前线程
    Thread t = Thread.currentThread();
    // 2、获取当前线程的threadlocals成员变量
    ThreadLocalMap map = getMap(t);
    // 3、判断map是否为null
    if (map != null)
        // 如果不为null,就直接将value放进map中
       // key是当前的threadLocal,value就是传进来的值
        map.set(this, value);
    else
        // 如果为 null,初始化一个map,再将value 放进map中
       // key是当前的threadLocal,value就是传进来的值
        createMap(t, value);
}

getMap()方法:返回当前线程的 threadLocals 变量

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

createMap()方法:进行 ThreadLocalMap 的初始化

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap的结构:ThreadLocalMap 是ThreadLocal下的一个内部类,ThreadLocalMap内还有一个Entry的内部类,并且继承了WeakReference,这里就是Java的弱引用,当堆空间不足时,会清理未被引用的entry。对了 ThreadLocalMap的key就是ThreadLocal,value就是我们想要保存的变量副本。

public class ThreadLocal<T>{
​
    static class ThreadLocalMap {
 
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
​
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

ThreadLocalMap的初始化方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 创建一个 Entry 数组
    table = new Entry[INITIAL_CAPACITY];
    // 计算hash值 这里的哈希冲突的解决办法采用了开放地址法,hash冲突的情况则下标挪一位再找
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 创建一个 Entry 放进Entry 数组
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //计算要调整大小的下一个大小值。
    setThreshold(INITIAL_CAPACITY);
}

小结:

每个对象最开始的 threadLocals 都为空,当线程调用 ThreadLocal.set() 或 ThreadLocal.get()时,就会调用到 createMap() 进行初始化。然后在当前线程里面,如果要使用副本变量,就可以通过 get() 在 threadLocals 里面查找。

image.png

图3:set()方法流程图

get()方法

public T get() {
    // 获取到当前线程
    Thread t = Thread.currentThread();
   // 2、获取当前线程的threadlocals成员变量
    ThreadLocalMap map = getMap(t);
    //3、判断map是否为null
    if (map != null) 
        //3.1、如果不为null,根据当前的ThreadLocal 从当前线程中的ThreadLocals中取出map存储的变量副本
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果存储的值不为null,就返回值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //
    return setInitialValue();
}

map.getEntry()简单说一下:

  1. 计算hash值,取值
  2. 不为null且相等则直接返回
  3. 否则按照hash冲突的解决方式继续寻找,直至最后找到返回结果或者返回null。

setInitialValue()方法:没有找到则初始化返回一个null值

private T setInitialValue() {
    T value = initialValue();// 这里初始化的是一个 null 值
    Thread t = Thread.currentThread(); 
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
// ThreadLocal类 → initialValue()()方法
protected T initialValue() {
    return null;
}

小结

image.png 图3:get()方法流程图

小结

总结起来就是:在每条线程 Thread 内部都有一个 ThreadLocal.ThreadLocalMap 类型的成员变量 threadLocals,这个 threadLocals 就是用来存变量副本的,其中的 key 就为当前 ThreadLocal 对象,value 为我们存储的变量副本。

image.png

图4:内部结构

自始至终,这些本地变量都不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量,那个线程私有的threadLocalMap 里面

ThreadLocal就是一个工具壳和一个key,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。

局限性

ThreadLocal设计的目的就是为每条线程都开辟一块自己的局部变量存储区域(并不是为了解决线程安全问题设计的,不过使用ThreadLocal可以避免一定的线程安全问题产生),但如果你需要将父线程中的数据共享给子线程时,就不怎么方便啦.

但是这种父线程传递信息给子线程的场景,我们使用的还是不少的,比如使用异步编程时,再或者是下面简单的场景

public class ThreadLocalExample2 {
​
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    public static void main(String[] args) {
        System.out.println("在主线程" + Thread.currentThread().getName() + "中保存临时用户信息");
        String userInfo = "宁在春";
        threadLocal.set(userInfo);
        new Thread(()->{
            // 获取不到父线程存储的信息
            System.out.println("在子线程" + Thread.currentThread().getName() + "中获取临时用户信息 " + threadLocal.get());
        },"MyThread2").start();
        threadLocal.remove();
    }
}
// 在主线程main中保存临时用户信息
// 在子线程pool-1-thread-1中获取临时用户信息 null

从输出结果中可以看到,子线程是无法获取到的,这是因为threadLocals就是存储在当前线程中而已。

然后就又有了InheritableThreadLocal的出现,继续吧。

注意事项

在继续往下之前,谈一个注意点,很多时候还会谈到ThreadLocal的副作用,脏数据和内存泄漏问题,但较真起来,这个问题更多的是开发时产生的问题。在每次使用 ThreadLocal 时,一定要记得在结束前及时调用 remove()方法清理数据

InheritableThreadLocal

很多时候,我们可能需要在线程中获取到父线程存储的相关信息,比如我们上面谈的那个简单例子,现在我们换使用InheritableThreadLocal看看可行不。

案例

public class InheritableThreadLocalExample {
​
    private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
​
    public static void main(String[] args) {
        System.out.println("在主线程" + Thread.currentThread().getName() + "中保存临时用户信息");
        String userInfo = "宁在春";
        threadLocal.set(userInfo);
        new Thread(()->{
            // 获取不到父线程存储的信息
            System.out.println("在子线程" + Thread.currentThread().getName() + "中获取临时用户信息 " + threadLocal.get());
        },"MyThread2").start();
        threadLocal.remove();
    }
}
//输出:
//在主线程main中保存临时用户信息
//在子线程MyThread2中获取临时用户信息 宁在春

怎么实现父子线程传值的?

要知道怎么实现的,无疑还是要去看createMap、set和get方法,看看它做了些什么改动。

之前我在类图中也说到了,Thread 中有这ThreadLocalMap threadLocals 和 inheritableThreadLocal 两个私有变量。

ThreadLocal.ThreadLocalMap threadLocals = null;
​
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal 是继承了ThreadLocal,并重写和实现了其中些许方法。

   private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

源码非常非常简短,或者说主要的逻辑还是在 Thread 和 ThreadLocal 中~

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
​
    // 1、获取父线程的数据
    protected T childValue(T parentValue) {
        return parentValue;
    }
    // 2、 获取 inheritableThreadLocals 变量
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
​
    // 3、为当前线程进行 inheritableThreadLocals 的初始化
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

在这个方法里面,只有T childValue()是我们在 ThreadLocal 中没有接触过的方法,那么肯定是有点妙用的。其他的就是从threadLocals改成了inheritableThreadLocals,没有太多改变。

真正的起点是在new Thread(() - >{})这段代码中,相信很多人,包括我在此之前都没有怎么看过Thread的构造函数过程

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
​
    this.name = name;
    // 获取当前执行线程作为父线程
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    // 省略一些检查相关的代码...
​
    // 将当前执行线程设置为创建出的线程的父线程
    this.group = g;
    
    // 省略了一些不是关注点的代码...
    
    // 我们需要关注的点
    // 判断 父线程的inheritThreadLocals 和 当前线程的 inheritThreadLocals 是否为 null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        // 不为null,才进行初始化,设置子线程中的inheritableThreadLocals变量
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    // 为创建出的线程分配默认线程栈大小
    this.stackSize = stackSize;
​
    // 设置线程ID
    tid = nextThreadID();
}

说起来,我们要关注的就是下面这一句

this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 传递的参数是:当前线程的inheritableThreadLocals变量
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
​
//  ThreadLocalMap类 私有构造函数
private ThreadLocalMap(ThreadLocalMap parentMap) {
    // 获取父线程中的存储的所有变量
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
​
    // 循环复制父线程中的Entry
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                //调用了 InheritableThreadLocal 重写的 childValue 方法
                // 获取到 e.value 值
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

小结

所以InheritableThreadLocal本质上就是通过复制来实现父子线程之间的传值

this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);完这一段代码后,子线程就已经存储了父线程的所有Entry信息了。

局限性:

InheritableThreadLocal 支持子线程访问父线程,本质上就是在创建线程的时候将父线程中的本地变量值全部复制到子线程中。

但是在谈到并发时,不可避免的会谈到线程池,因为线程的频繁创建和销毁,对于程序来说,代价实在太大。

而在线程池中,线程是复用的,并不用每次新建,那么此时InheritableThreadLocal复制的父线程就变成了第一个执行任务的线程了,即后面所有新建的线程,他们所访问的本地变量都源于第一个执行任务的线程(期间也可能会遭遇到其他线程的修改),从而造成本地变量混乱

比如:

假如我们有这样的一个流程,10个请求到达controller,然后调用service,在service中我们还要执行一个异步任务,最后等待结果的返回。

10个service - > 10个异步任务 ,在service,我们会设置一个变量副本,在执行异步任务的子线程中,需要get出来进行调用。

public class InheritableThreadLocalDemo3 {
​
    /**
     * 业务线程池,service 中执行异步任务的线程池
     */
    private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);
​
    /**
     * 线程上下文环境,在service中设置环境变量,
     * 然后在这里提交一个异步任务,模拟在子线程(执行异步任务的线程)中,是否可以访问到刚设置的环境变量值。
     */
    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();
​
    public static void main(String[] args) {
        // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
        for (int i = 0; i < 10; i++) {
            // 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
            new Thread(new ServiceThread(i)).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //关闭线程池
        businessExecutors.shutdown();
    }
​
    /**
     * 模拟Service业务代码
     */
    static class ServiceThread implements Runnable {
        private int i;
​
        public ServiceThread(int i) {
            this.i = i;
        }
​
        @Override
        public void run() {
            requestIdThreadLocal.set(i);
            System.out.println("执行service方法==>在"+Thread.currentThread().getName() + "中存储变量副本==>" + i);
            // 异步编程 CompletableFuture.runAsync()创建无返回值的简单异步任务,businessExecutors 表示线程池~
            CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
                try {
                    // 模拟执行时间
                    Thread.sleep(500L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:"+requestIdThreadLocal.get());
            }, businessExecutors);
            requestIdThreadLocal.remove();
        }
    }
}

输出:

执行service方法==>在Thread-0中存储变量副本==>0
执行service方法==>在Thread-1中存储变量副本==>1
执行service方法==>在Thread-6中存储变量副本==>6
执行service方法==>在Thread-3中存储变量副本==>3
执行service方法==>在Thread-4中存储变量副本==>4
执行service方法==>在Thread-5中存储变量副本==>5
执行service方法==>在Thread-2中存储变量副本==>2
执行service方法==>在Thread-7中存储变量副本==>7
执行service方法==>在Thread-8中存储变量副本==>8
执行service方法==>在Thread-9中存储变量副本==>9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:1
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:7
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:2
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:5
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:7
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:2
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:5
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:1

可以看到在子线程中获取到的变量值已经重复~ 此时线程变量副本值已经错乱啦。

然后接着就出现了TransmittableThreadLocal啦,接着看吧,看看他们是怎么解决的

TransmittableThreadLocal

TransmittableThreadLocal 是alibaba 开源的一个工具类,github地址,是用于解决 “在使用线程池等会缓存线程的组件情况下传递ThreadLocal” 问题的 InheritableThreadLocal 扩展工具类。

回到上面的问题,我们使用 TransmittableThreadLocal 来改造一下上面的问题吧

添加相关依赖:

<!--https://github.com/alibaba/transmittable-thread-local/releases/tag/v2.14.2-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.2</version>
</dependency>

使用方式

使用方式一:使用transmittable-thread-localjar中的相关包装类,或者相关api对使用的相关类进行包装(此处包装两字只是我个人的形容,严格说起来是用了设计模式中的装饰器模式,还有也用到了模板方法模式)

装饰器模式:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式

public class TransmittableThreadLocalDemo1 {
​
    /**
     * 业务线程池,service 中执行异步任务的线程池
     * 使用 TtlExecutors.getTtlExecutorService() 包装一下我们自己的线程池,这样才可以 使用 TransmittableThreadLocal 解决在使用线程池等会缓存线程的组件情况下传递ThreadLocal的问题
     */
    private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));
​
    /**
     * 线程上下文环境,改为使用 TransmittableThreadLocal 来保存
     * 然后在这里提交一个异步任务,模拟在子线程(执行异步任务的线程)中,是否可以访问到刚设置的环境变量值。
     */
    private static TransmittableThreadLocal<Integer> requestIdThreadLocal = new TransmittableThreadLocal<>();
​
    public static void main(String[] args) {
        // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
        for (int i = 0; i < 10; i++) {
            // 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
            new Thread(new ServiceThread(i)).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //关闭线程池
        businessExecutors.shutdown();
    }
​
    /**
     * 模拟Service业务代码
     */
    static class ServiceThread implements Runnable {
        private int i;
​
        public ServiceThread(int i) {
            this.i = i;
        }
​
        @Override
        public void run() {
            requestIdThreadLocal.set(i);
            System.out.println("执行service方法==>在"+Thread.currentThread().getName() + "中存储变量副本==>" + i);
            // 异步编程 CompletableFuture.runAsync()创建无返回值的简单异步任务,businessExecutors 表示线程池~
            CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
                try {
                    // 模拟执行时间
                    Thread.sleep(500L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:"+requestIdThreadLocal.get());
            }, businessExecutors);
            requestIdThreadLocal.remove();
        }
    }
}

输出结果:

执行service方法==>在Thread-2中存储变量副本==>1
执行service方法==>在Thread-1中存储变量副本==>0
执行service方法==>在Thread-5中存储变量副本==>4
执行service方法==>在Thread-3中存储变量副本==>2
执行service方法==>在Thread-4中存储变量副本==>3
执行service方法==>在Thread-6中存储变量副本==>5
执行service方法==>在Thread-8中存储变量副本==>7
执行service方法==>在Thread-9中存储变量副本==>8
执行service方法==>在Thread-7中存储变量副本==>6
执行service方法==>在Thread-10中存储变量副本==>9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:0
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:8
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:3
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:7
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:6
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:1
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:5
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:4
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:2

都是唯一的,不再有重复的值

使用方式二:添加JVM的启动参数

idea 启动

image.png

-javaagent:path/transmittable-thread-local-xxxx.jar

使用-javaagent近似无侵入式的使用TransmittableThreadLocal.

public class TransmittableThreadLocalDemo2 {
​
    /**
     * 业务线程池,service 中执行异步任务的线程池
     */
    private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);
​
    /**
     * 线程上下文环境,在service中设置环境变量,
     * 然后在这里提交一个异步任务,模拟在子线程(执行异步任务的线程)中,是否可以访问到刚设置的环境变量值。
     */
    private static TransmittableThreadLocal<Integer> requestIdThreadLocal = new TransmittableThreadLocal<>();
​
    public static void main(String[] args) {
        // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
        for (int i = 0; i < 10; i++) {
            // 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
            new Thread(new ServiceThread(i)).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //关闭线程池
        businessExecutors.shutdown();
    }
​
    /**
     * 模拟Service业务代码
     */
    static class ServiceThread implements Runnable {
        private int i;
​
        public ServiceThread(int i) {
            this.i = i;
        }
​
        @Override
        public void run() {
            requestIdThreadLocal.set(i);
            System.out.println("执行service方法==>在"+Thread.currentThread().getName() + "中存储变量副本==>" + i);
            // 异步编程 CompletableFuture.runAsync()创建无返回值的简单异步任务,businessExecutors 表示线程池~
            CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
                try {
                    // 模拟执行时间
                    Thread.sleep(500L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:"+requestIdThreadLocal.get());
            }, businessExecutors);
            requestIdThreadLocal.remove();
        }
    }
}

你可以试着添加和不添加都试一下,就可以看出效果来啦。

jar包方式启动的话,也是一样,在启动参数上加上-javaagent:path/transmittable-thread-local-xxxx.jar即可。

注意:-javaagent:path/transmittable-thread-local-xxxx.jar要放在第一个启动参数中。

相关issue:TTL agent 与 其他agent的兼容性问题

javaagent不太熟悉,不过后面参考文档中有贴出来我好奇时看了的相关优质文章,大家感兴趣的话也可以看看,值得了解的。

浅浅的分析了点内容

在第一个案例中,使用 TransmittableThreadLocal 时,我们也使用了 TtlExecutors.getTtlExecutorService()对我们的线程池做了增强(这也是必须的搭配,否则没法使用 TransmittableThreadLocal 特性)

    private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));

那到底做了一些什么事情呢?然后就又可以让TransmittableThreadLocal解决在使用线程池等会缓存线程的组件情况下传递ThreadLocal的问题呢?

浅浅的看一下哈~

我们先看看它针对我们使用的线程做了什么增强

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

在这里又看到new ExecutorServiceTtlWrapper(executorService, true)返回~ 还得继续往下看

class ExecutorServiceTtlWrapper extends ExecutorTtlWrapper implements ExecutorService, TtlEnhanced {
    private final ExecutorService executorService;
​
    ExecutorServiceTtlWrapper(@NonNull ExecutorService executorService, boolean idempotent) {
        super(executorService, idempotent);
        this.executorService = executorService;
    }
    
    //.....
}

这就是一个构造函数,那么最后我们使用的线程池也就是这个增强后的ExecutorServiceTtlWrapper了。它在这里也实现了ExecutorService接口,那么肯定是实现了里面的所有方法。

我们直接跳到submit方法,看看它做了什么操作,让它得以增强吧。

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

在这里能看到transmittable-thread-local是对我们用到的Runnable、Callable都进行包装增强。

这里我们只去看看TtlRunnable,不对Callable继续深入的谈啦。

虽然还没看TtlRunnable的代码,但是到这里我们也大致能猜到,transmittable-thread-local对我们使用到的相关类都进行新的实现,并且兼容原本的方式。

TtlRunnable 代码
public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
    
    private final AtomicReference<Object> capturedRef;
​
    private final Runnable runnable;
    // 运行后是否 释放 Ttl 值的引用
    private final boolean releaseTtlValueReferenceAfterRun;
​
    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        // capture() 这里具体调用的是 TransmittableThreadLocal下内部类Transmitter的capture()方法
        // 捕获当前线程中的所有TransmittableThreadLocal和注册的ThreadLocal值。
        // 之后会详细看到滴
        this.capturedRef = new AtomicReference<>(capture());
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }
​
    @Override
    public void run() {
        //获取所有的ttl及tl快照内容 即获取到主线程传递下来的ThreadLocal的值。
        final Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        //重放从capture()捕获的TransmittableThreadLocal和注册的ThreadLocal值,并在重放之前返回当前线程中的备份TransmittableThreadLocal值。
        /**
         * 1.  backup(备份)是子线程已经存在的ThreadLocal变量;
         * 2. 将captured的ThreadLocal值在子线程中set进去;
         */
        final Object backup = replay(captured); 
        try {
            //执行线程的任务
            runnable.run();
        } finally {
            //从replay(Object) / clear()恢复备份的TransmittableThreadLocal和注册的ThreadLocal值。
            // 恢复线程执行replay方法之前的TTL值
            restore(backup);
        }
    }
​
    //.... 
}

小结:

  1. 获取所有的ttl及tl快照内容
  2. 获取到捕获的TransmittableThreadLocal和注册的ThreadLocal值,并返回当前子线程所有存在的变量
  3. 执行线程任务
  4. 恢复线程到执行replay方法之前的TTL值

关于captured/replay/restore 这三个方法,我推荐你看作者回复的issue和文档:

没有特别理解 capture replay restore 这样的方式的好处? #145

所有TTL值的抓取、回放和恢复方法(即CRR操作)

经典的设计~ 值得搬个小板凳过去观摩

有想贴出capture replay restore这三个的方法的,但是功底太浅,还是浅浅的写点文字吧~

为什么线程要恢复到执行replay方法之前的TTL值?

// 恢复线程执行replay方法之前的TTL值
restore(backup);

因为在子线程中可能会修改ThreadLocal的值,另外restore里面会主动调用remove()回收,避免内存泄露(会删除子线程新增的TTL)

有下列两种情况:

  1. 一种情况是:主线程启动了一个异步任务,此时主线程和子线程会并行,由于父子线程的数据是隔离开的,子线程此时对TTL中的内容进行修改并不会影响到原线程的逻辑
  2. 另一种情况是:线程池的拒绝策略为CallerRunsPolicy时,那么在主线程内启动这个异步任务可能会有当前的主线程来执行,那么线程之间的数据并不会隔离,那么如果对ThreadLocal中的数据进行了修改,那么将会影响到程序的正常运行。

想浅浅的摆个烂啦,后面的内容还有很多,大家感兴趣继续去肝

看相关博文的时候,都谈到了下面这段经典代码

private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
    new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
    @Override
    protected WeakHashMap<TransmittableThreadLocalCode<Object>, ?> initialValue() {
        return new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>();
    }
​
    @Override
    protected WeakHashMap<TransmittableThreadLocalCode<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocalCode<Object>, ?> parentValue) {
        return new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>(parentValue);
    }
};

这里就是保存TTL的地方,它结合TTL中静态内部类 Transmitter,实现了线程池级别的缓存。

小小的小结一下:

  1. TTL通过增强Runnable,将原本位于new Thread()#init()的变量副本的传递,推迟到线程任务执行的时候,即在run()中,这样即使是使用线程池的线程,也能够在使用的时候将线程的变量副本继续传递下去。
  2. 第二点,通过captured/replay/restor捕获、重放和回放机制,避免了在高并发情况下,线程池在CallerRunsPolicy拒绝策略下,启动的异步线程和主线程在同一线程内执行,因为子线程修改线程的变量副本从而导致业务数据混乱的问题。

还没完全弄懂的我,就不继续摆弄那点浅薄的知识啦~

不过在这里我倒是学到了WeakHashMap,有让我感觉到学习JVM的用处,想去看JVM那本书啦,哈哈,学习Java真的是任重而道远啊(卷不动啦,想摆烂)

让人有收获的 issue

之前就谈到了,我是因为看到文章谈到了TTL,然后在TTL上又碰巧看到了感兴趣的 issue,看那些issue,我是越看越着迷,哈哈,也因此有了不少收获。

  1. 分布式追踪系统场景下,如何使用TTL #53

    作者大大回复的特别认真,提出问题的大佬也非常强,讨论了很长的篇幅,推荐观摩

    还有关于『分布式跟踪系统』可以了解一下GoogleDapper(介绍的论文:中文| 英文),推荐阅读,写的很好。

  2. TTL agent 与 其他agent的兼容性问题 #226

    也很有意思,无论是作者还是提问者,还有其他参与的大佬,执行力都非常强

  3. Issue: 能否提供与LOG4J(2)中的MDC集成或增强 @bwzhang2011

  4. Issue: slf4j MDCAdapter with multi-thread-context 支持 @bwzhang2011

    这两处可能需要了解一下Log4j或slf4j 中的MDC,不过也很实用,也很有讨论氛围,最后作者也给出了相关建议和相关集成的包,真的很赞

安利一波作者-李鼎,每个issue回复的都好认真,给出来的回复和建议都很有帮助。

注意项

TTL是存在线程安全问题的,因为默认都是引用类型拷贝,如果子线程修改了数据,主线程是可以感知到的。

总结图

也是看到了这里,那么最后再通过这张思维图作为这篇博客正文的的结束吧。

image.png

也试着回顾一下,看完这篇博客是否有收获吧,有的话也请给俺给个赞吧

后记

这个周末就跟它三好好杠完了,我觉得还是挺有意义的。

ThreadLocalInheritableThreadLocal再到TransmittableThreadLocal,之前懂的不多,写了,测试了,动手了,然后再记录下这个过程后,对于它们也终于不再是之前那般懵懂啦。

参考文档

在了解和学习TransmittableThreadLocal所拜读过的文章

讲透 ThreadLocal 和 InheritableThreadLocal

InheritableThreadLocal在全链路中的作用

TransmittableThreadLocal原理解析

TransmittableThreadLocal解决线程池变量传递以及原理解析

多线程篇-TransmittableThreadLocal解决池化复用线程的传值问题

通过transmittable-thread-local源码理解线程池线程本地变量传递的原理

Java字节码技术(二)字节码增强之ASM、JavaAssist、Agent、Instrumentation

字节码增强技术探索-美团技术团队

从TransmittableThreadLocal使用前调研(源码分析)

搞定 WeakHashMap 的工作原理一篇文章就够了

以及 github上的 TransmittableThreadLocal 的 issue

求职

一年工作经验,熟悉主流Java框架

base:广州、深圳、杭州

🤕 大佬我很好带的

联系方式wx:nzc_wyh 邮箱:nzc_wyh@163.com