从ThreadLocal谈到TransmittableThreadLocal,从使用到原理1:https://developer.aliyun.com/article/1394835
局限性
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
啦,接着看吧,看看他们是怎么解决的
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理3:https://developer.aliyun.com/article/1394838