一、前言
前一段时间做了个服务调用链路追踪的需求,最后需要把trace信息通过Mybatis Plugin持久化到每个业务表中;以供后面的日志审计服务使用。
其中我采用ThreadLocal把trace信息传递到Mybatis Plugin中,不过由于存在 在dubbo接口 / Rest接口中采用new Threa()、CompleteFuture的方式启动一个异步线程去做DB数据(走MyBatis)的持久化,这时ThreadLocal就无法将trace信息传递到Mybatis Plugin,因此有了今天InheritableThreadLocal
的故事。
二、InheritableThreadLocal概述
InheritableThreadLocal
用于实现 父线程生成的变量可以自动传递到子线程中进行使用 的需求。
从类的构造来看,InheritableThreadLocal
继承自ThreadLocal
;
- 在编码使用上,InheritableThreadLocal和ThreadLocal没有任何区别,只是在实例化的对象的时候使用new InheritableThreadLocal
<>
()。
1、ThreadLocal的使用
public class ParentChildThreadLocal {
public final static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("haha, parent-child variables!");
System.out.println("父线程的值(threadLocal):" + threadLocal.get());
// 开启子线程
new Thread(() -> {
System.out.println("子线程继承到的值(threadLocal): " + threadLocal.get());
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码中,实例化一个ThreadLocal变量,在main线程中对ThreadLocal变量进行赋值、取值,并在main线程中开启一个子线程获取main线程中ThreadLocal变量的值。执行结果如下:
从运行结果可以看出,使用ThreadLocal
时,子线程无法获取到父线程中的本地变量值。
2、InheritableThreadLocal的使用
public class ParentChildThreadLocal {
public final static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
inheritableThreadLocal.set("haha, parent-child variables!");
System.out.println("父线程的值(InheritableThreadLocal):" + inheritableThreadLocal.get());
// 开启子线程
new Thread(() -> {
System.out.println("子线程继承到的值(InheritableThreadLocal): " + inheritableThreadLocal.get());
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码中,实例化一个InheritableThreadLocal变量,在main线程中对InheritableThreadLocal变量进行赋值、取值,并在main线程中开启一个子线程获取main线程中InheritableThreadLocal变量的值。执行结果如下:
从运行结果可以看出,使用InheritableThreadLocal
之后,子线程可以获取到父线程中的本地变量值。
那么为什么呢?下面来跟一下InheritableThreadLocal的源码。
三、InheritableThreadLocal源码分析
经过上面的测试代码,我们可以推测出InheritableThreadLocal是ThreadLocal派生出专门用于解决线程本地变量父传子问题的,这里我们通过源码分析一下InheritableThreadLocal是如何完成这一操作的!
1、Thread类构成
Thread类中包含 threadLocals 和 inheritableThreadLocals 两个变量,其中 inheritableThreadLocals 用于存储可自动从父线程向子线程中传递的ThreadLocal.ThreadLocalMap。
1> Thread#inheritableThreadLocals变量
接着,来看一下创建线程时如何实现ThreadLocal在父子线程之间的传递;
而此处所谓的父线程,指想调用new Thread()实例化一个线程的当前线程。
2> ThreadLocal#createInheritedMap()
再看ThreadLocal#createInheritedMap()方法是如何将父线程中的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);
// ThreadLocalMap 使用 Entry[] table 存储ThreadLocal
table = new Entry[len];
// 逐一复制 parentMap中的所有ThreadLocal记录
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) {
// childValue()内部是直接将e.value返回
Object value = key.childValue(e.value);
// 构建Entry数组节点,放入到ThreadLocalMap中
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
// 使用开发地址方法解决hash冲突
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
由于ThreadLocalMap
内部存在hash冲突的情况,所有采用开发地址法
解决hash冲突问题。
- 即数据想放在索引下标为3,但是索引下标3处存在数据,此时就从索引下标4开始往后查找空闲的位置。相应的它的查询速度就会慢很多,这是一种时间换空间的做法。
从这里可以推测出,InheritableThreadLocal
主要便是针对Thread类的inheritableThreadLocals
变量做操作。
2、InheritableThreadLocal类结构
首先,InheritableThreadLocal类继承自ThreadLocal;
然后重写了ThreadLocal的三个方法:childValue(T parentValue)
、getMap(Thread t)
、createMap(Thread t, T firstValue)
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
1、ThreadLocal#set()方法
走到ThreadLocal#set()方法里:
上面提到,InheritableThreadLocal重写了ThreadLocal的三个方法,其中就包括getMap(Thread t)
、createMap(Thread t, T firstValue)
;也就是说ThreadLocal存储数据的主体从Thread的threadLocals
变量变成了inheritableThreadLocals
变量;
而对于其余操作并无区别;
- 因为hash计算、查找、扩容都是ThradLocalMap里做的,
- 而InheritableThreadLocal只决定为Thread对象里哪个ThreadLocalMap属性赋值。
2、ThreadLocal#get()方法
ThreadLocal#get()方法和set()方法一样,只是操作的Thread的ThradLocalMap对象不同。
3、阶段总结,问:InheritableThreadLocal和ThreadLocal的区别是什么?
InheritableThreadLocal
继承自ThreadLocal
,用于解决父子线程之间传递变量的问题。而
InheritableThreadLocal
之所以能在父子线程之间传递变量,是因为其使用的存储线程ThreadLocal数据的主体ThreadLocalMap
是Thread类中的inheritableThreadLocals
,而ThreadLocal
使用的是Thread类中的threadLocals
。
- 在线程初始化的时候,会从父线程中取出
inheritableThreadLocals
信息传递到子线程。- 所以InheritableThreadLocal和ThreadLocal的唯一区别在于:它俩存储数据的
ThreadLocalMap
主体不同。
三、InheritableThreadLocal有什么问题吗?
1、直接通过set改变对象内容时,线程不安全
如果线程本地变量是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量(本质上是同一个对象)。
这里对于共享变量的修改存在线程安全问题太正常了。主要点在于线程池中InheritableThreadLocal失效。
2、线程池中InheritableThreadLocal失效
在使用线程池时,InheritableThreadLocal会完全失效;因为父线程的ThreadLocalMap是通过实例化一个Thread时赋值给子线程的,而线程池在执行异步任务时可能不需要创建新的线程,因此也就不会再传递父线程的ThreadLocalMap给子线程。
针对这个问题,我们可以通过使用阿里开源的TransmittableThreadLocal
解决,参考文档:https://github.com/alibaba/transmittable-thread-local。
具体内容下篇博文输出。