文章目录
一、ThreadLocal使用及原理解析
项目中我们如果想要某个对象在程序运行中的任意位置获取到,就需要借助ThreadLocal来实现,这个对象称作线程的本地变量,下面就介绍下ThreadLocal是如何做到线程内本地变量传递的
1、基本使用
先来看下基本用法:
private static ThreadLocal tl = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
tl.set(1);
System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: %s",
Thread.currentThread().getName(), tl.get()));
fc();
new Thread(ThreadLocalTest::fc).start();
}
private static void fc() {
System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: %s",
Thread.currentThread().getName(), tl.get()));
}
运行结果:
当前线程名称: main, main方法内获取线程内数据为: 1
当前线程名称: main, fc方法内获取线程内数据为: 1
当前线程名称: Thread-0, fc方法内获取线程内数据为: null
可以看到,main线程内任意地方都可以通过ThreadLocal获取到当前线程内被设置进去的值,而被异步出去的fc调用,却由于替换了执行线程,而拿不到任何数据值,那么我们现在再来改造下上述代码,在异步发生之前,给Thread-0线程也设置一个上下文数据:
private static ThreadLocal tl = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
tl.set(1);
System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: %s",
Thread.currentThread().getName(), tl.get()));
fc();
new Thread(()->{
tl.set(2); //在子线程里设置上下文内容为2
fc();
}).start();
Thread.sleep(1000L); //保证下面fc执行一定在上面异步代码之后执行
fc(); //继续在主线程内执行,验证上面那一步是否对主线程上下文内容造成影响
}
private static void fc() {
System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: %s",
Thread.currentThread().getName(), tl.get()));
}
运行结果为:
当前线程名称: main, main方法内获取线程内数据为: 1
当前线程名称: main, fc方法内获取线程内数据为: 1
当前线程名称: Thread-0, fc方法内获取线程内数据为: 2
当前线程名称: main, fc方法内获取线程内数据为: 1
可以看到,主线程和子线程都可以获取到自己的那份上下文里的内容,而且互不影响。
2、原理分析
ok,上面通过一个简单的例子,我们可以了解到ThreadLocal(以下简称TL)具体的用法,这里先不讨论它实质上能给我们带来什么好处,先看看其实现原理,等这些差不多了解完了,我再通过我曾经做过的一个项目,去说明TL的作用以及在企业级项目里的用处。
我以前在不了解TL的时候,想着如果让自己实现一个这种功能的轮子,自己会怎么做,那时候的想法很单纯,觉得通过一个Map就可以解决,Map的key设置为Thread.currentThread(),value设置为当前线程的本地变量即可,但后来想想就觉得不太现实了,实际项目中可能存在大量的异步线程,对于内存的开销是不可估量的,而且还有个严重的问题,线程是运行结束后就销毁的,如果按照上述的实现方案,map内是一直持有这个线程的引用的,导致明明执行结束的线程对象不能被jvm回收,造成内存泄漏,时间久了,会直接OOM。
所以,java里的实现肯定不是这么简单的,下面,就来看看java里的具体实现吧。
先来了解下,TL的基本实现,为了避免上述中出现的问题,TL实际上是把我们设置进去的值以k-v的方式放到了每个Thread对象内(TL对象做k,设置的值做v),也就是说,TL对象仅仅起到一个标记、对Thread对象维护的map赋值的作用。
先从set方法看起:
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocal.ThreadLocalMap map = getMap(t); //获取到当前线程持有的ThreadLocalMap对象
if (map != null)
map.set(this, value); //直接set值,具体方法在下面
else
createMap(t, value); // 为空就给当前线程创建一个ThreadLocalMap对象,赋值给Thread对象,具体方法在下面
}
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //每个线程都有一个ThreadLocalMap,key为TL对象(其实是根据对象hash计算出来的值),value为该线程在此TL对象下存储的内容值
}
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table; //获取存储k-v对象的数组(散列表)
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //根据TL对象的hashCode(也是特殊计算出来的,保证每个TL对象的hashCode不同)计算出下标
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) { //线性探查法解决哈希冲突问题,发现下标i已经有Entry了,则就查看i+1位置处是否有值,以此类推
ThreadLocal<?> k = e.get(); //获取k
if (k == key) { //若k就是当前TL对象,则直接为其value赋值
e.value = value;
return;
}
if (k == null) { //若k为空,则认为是可回收的Entry,则利用当前k和value组成新的Entry替换掉该可回收Entry
replaceStaleEntry(key, value, i);
return;
}
}
//for循环执行完没有终止程序,说明遇到了空槽,这个时候直接new对象赋值即可
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) //这里用来清理掉k为null的废弃Entry
rehash(); //如果没有发生清除Entry并且size超过阈值(阈值 = 最大长度 * 2/3),则进行扩容
}
//直接为当前Thread初始化它的ThreadLocalMap对象
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY]; //初始化数组
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //计算初始位置
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue); //因为初始化不存在hash冲突,直接new
size = 1;
setThreshold(INITIAL_CAPACITY); //给阈值赋值,上面已经提及,阈值 = 最大长度 * 2/3
}
通过上述代码,我们大致了解了TL在set值的时候发生的一些操作,结合之前说的,我们可以确定的是,TL其实对于线程来说,只是一个标识,而真正线程的本地变量被保存在每个线程对象的ThreadLocalMap里,这个map里维护着一个Entry[]的数组(散列表),Entry是个k-v结构的对象,k为TL对象,v为对应TL保存在该线程内的本地变量值,值得注意的是,这里的k针对TL对象的引用是个弱引用,来看下源码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
为什么这里需要弱引用呢?我们先来看一张图,结合上面的介绍和这张图,来了解TL和Thread间的关系:
图中虚线表示弱引用,那么为什么要这么做呢?
简单来说,一个TL对象被创建出来,并且被一个线程放到自己的ThreadLocalMap里,假如TL对象失去原有的强引用,但是该线程还没有死亡,如果k不是弱引用,那么就意味着TL并不能被回收,现在k为弱引用,那么在TL失去强引用的时候,gc可以直接回收掉它,弱引用失效,这就是上面代码里会进行检查,k=null的清除释放内存的原因(这个可以参考下面expungeStaleEntry
方法,而且set、get、remove都会调用该方法,这也是TL防止内存泄漏所做的处理)。
综上,简单来说这个弱引用
就是用来解决由于使用TL不当导致的内存泄漏
问题的,假如没有弱引用,那么你又用到了线程池(池化后线程不会被销毁
),然后TL对象又是局部
的,那么就会导致线程池内线程里的ThreadLocalMap
存在大量的无意义的TL对象引用,造成过多无意义的Entry对象,因为即便调用了set、get等方法检查k=null,也没有作用,这就导致了内存泄漏
,长时间这样最终可能导致OOM,所以TL的开发者为了解决这种问题,就将ThreadLocalMap里对TL对象的引用改为弱引用
,一旦TL对象失去强引用
,TL对象就会被回收,那么这里的弱引
用指向的值就为null,结合上面说的,调用操作方法时会检查k=null的Entry进行回收,从而避免了内存泄漏的可能性。
因为TL解决了内存泄漏的问题,因此即便是局部变量的TL对象且启用线程池技术,也比较难造成内存泄漏的问题,而且我们经常使用的场景就像一开始的示例代码一样,会初始化一个全局的static的TL对象
,这就意味着该对象在程序运行期间都不会存在强引用消失的情况,我们可以利用不同的TL对象给不同的Thread里的ThreadLocalMap赋值,通常会set值(覆盖原有值),因此在使用线程池的时候也不会造成问题,异步开始之前set值,用完以后remove,TL对象可以多次得到使用,启用线程池的情况下如果不这样做,很可能业务逻辑也会出问题(一个线程存在之前执行程序时遗留下来的本地变量,一旦这个线程被再次利用,get时就会拿到之前的脏值);
说完了set,我们再来看下get:
public T get() {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t); //获取线程内的ThreadLocalMap对象
if (map != null) {
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this); //根据当前TL对象(key)获取对应的Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result; //直接返回value即可
}
}
return setInitialValue(); //如果发现当前线程还没有ThreadLocalMap对象,则进行初始化
}
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1); //计算下标
ThreadLocal.ThreadLocalMap.Entry e = table[i];
if (e != null && e.get() == key) //根据下标获取的Entry对象如果key也等于当前TL对象,则直接返回结果即可
return e;
else
return getEntryAfterMiss(key, i, e); //上面说过,有些情况下存在下标冲突的问题,TL是通过线性探查法来解决的,所以这里也一样,如果上面没找到,则继续通过下标累加的方式继续寻找
}
private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get(); //继续累加下标的方式一点点的往下找
if (k == key) //找到了就返回出去结果
return e;
if (k == null) //这里也会检查k==null的Entry,满足就执行删除操作
expungeStaleEntry(i);
else //否则继续累加下标查找
i = nextIndex(i, len);
e = tab[i];
}
return null; //找不到返回null
}
//这里也放一下nextIndex方法
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
最后再来看看remove方法:
public void remove() {
ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this); //清除掉当前线程ThreadLocalMap里以当前TL对象为key的Entry
}
private void remove(ThreadLocal<?> key) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //计算下标
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) { //找到目标Entry
e.clear(); //清除弱引用
expungeStaleEntry(i); //通过该方法将自己清除
return;
}
}
}
private int expungeStaleEntry(int staleSlot) { //参数为目标下标
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null; //首先将目标value清除
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
ThreadLocal.ThreadLocalMap.Entry e;
int i;
// 由目标下标开始往后逐个检查,k==null的清除掉,不等于null的要进行rehash
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
目前主要方法set、get、remove已经介绍完了,包含其内部存在的弱引用的作用,以及实际项目中建议的用法,以及为什么要这样用,也进行了简要的说明
二、InheritableThreadLocal的使用及原理解析
1、基本使用
我们继续来看之前写的例子:
private static ThreadLocal tl = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
tl.set(1);
System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: %s",
Thread.currentThread().getName(), tl.get()));
fc();
new Thread(() -> {
fc();
}).start();
Thread.sleep(1000L); //保证下面fc执行一定在上面异步代码之后执行
fc(); //继续在主线程内执行,验证上面那一步是否对主线程上下文内容造成影响
}
private static void fc() {
System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: %s",
Thread.currentThread().getName(), tl.get()));
}
输出为:
当前线程名称: main, main方法内获取线程内数据为: 1
当前线程名称: main, fc方法内获取线程内数据为: 1
当前线程名称: Thread-0, fc方法内获取线程内数据为: null
当前线程名称: main, fc方法内获取线程内数据为: 1
我们会发现,父线程的本地变量是无法传递给子线程的,这当然是正常的,因为线程本地变量来就不应该相互有交集,但是有些时候,我们的确是需要子线程里仍然可以获取到父线程里的本地变量,现在就需要借助TL的一个子类:InheritableThreadLocal(下面简称ITL),来完成上述要求 现在我们将例子里的
private static ThreadLocal tl = new ThreadLocal<>();
改为:
private static ThreadLocal tl = new InheritableThreadLocal<>();
然后我们再来运行下结果:
当前线程名称: main, main方法内获取线程内数据为: 1
当前线程名称: main, fc方法内获取线程内数据为: 1
当前线程名称: Thread-0, fc方法内获取线程内数据为: 1
当前线程名称: main, fc方法内获取线程内数据为: 1
可以发现,子线程里已经可以获得父线程里的本地变量了。
结合之前讲的TL的实现,简单理解起来并不难,基本可以认定,是在创建子线程的时候,父线程的ThreadLocalMap(下面简称TLMap)里的值递给了子线程,子线程针对上述tl对象持有的k-v进行了copy,其实这里不是真正意义上对象copy,只是给v的值多了一条子线程TLMap的引用而已,v的值在父子线程里指向的均是同一个对象,因此任意线程改了这个值,对其他线程是可见的,为了验证这一点,我们可以改造以上测试代码:
private static ThreadLocal tl = new InheritableThreadLocal<>();
private static ThreadLocal tl2 = new InheritableThreadLocal<>();
public static void main(String[] args) throws Exception {
tl.set(1);
Hello hello = new Hello();
hello.setName("init");
tl2.set(hello);
System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: tl = %s,tl2.name = %s",
Thread.currentThread().getName(), tl.get(), tl2.get().getName()));
fc();
new Thread(() -> {
Hello hello1 = tl2.get();
hello1.setName("init2");
fc();
}).start();
Thread.sleep(1000L); //保证下面fc执行一定在上面异步代码之后执行
fc(); //继续在主线程内执行,验证上面那一步是否对主线程上下文内容造成影响
}
private static void fc() {
System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: tl = %s,tl2.name = %s",
Thread.currentThread().getName(), tl.get(), tl2.get().getName()));
}
输出结果为:
当前线程名称: main, main方法内获取线程内数据为: tl = 1,tl2.name = init
当前线程名称: main, fc方法内获取线程内数据为: tl = 1,tl2.name = init
当前线程名称: Thread-0, fc方法内获取线程内数据为: tl = 1,tl2.name = init2
当前线程名称: main, fc方法内获取线程内数据为: tl = 1,tl2.name = init2
可以确认,子线程里持有的本地变量跟父线程里那个是同一个对象。
2、原理分析
通过上述的测试代码,基本可以确定父线程的TLMap被传递到了下一级,那么我们基本可以确认ITL是TL派生出来专门解决线程本地变量父传子问题的,那么下面通过源码来分析一下ITL到底是怎么完成这个操作的。
先来了解下Thread类,上节说到,其实最终线程本地变量是通过TLMap存储在Thread对象内的,那么来看下Thread对象内关于TLMap的两个属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Thread类里其实有两个TLMap属性,第一个就是普通TL对象为其赋值,第二个则由ITL对象为其赋值,来看下TL的set方法的实现,这次针对该方法介绍下TL子类的相关方法实现:
// TL的set方法,如果是子类的实现,那么获取(getMap)和初始化赋值(createMap)都是ITL对象里的方法
// 其余操作不变(因为hash计算、查找、扩容都是TLMap里需要做的,这里子类ITL只起到一个为Thread对象里哪个TLMap属性赋值的作用)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// ITL里getMap方法的实现
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals; //返回的其实是Thread对象的inheritableThreadLocals属性
}
// ITL里createMap方法的实现
void createMap(Thread t, T firstValue) {
// 也是给Thread的inheritableThreadLocals属性赋值
t.inheritableThreadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
而inheritableThreadLocals里的信息通过Thread的init方法是可以被传递下去的:
// 初始化一个Thread对象时的代码段(Thread类的init方法)
Thread parent = currentThread();
if (parent.inheritableThreadLocals != null){ //可以看到,如果父线程存在inheritableThreadLocals的时候,会赋值给子线程(当前正在被初始化的线程)
// 利用父线程的TLMap对象,初始化一个TLMap,赋值给自己的inheritableThreadLocals(这就意味着这个TLMap里的值会一直被传递下去)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// 看下TL里对应的方法
static ThreadLocal.ThreadLocalMap createInheritedMap(ThreadLocal.ThreadLocalMap parentMap) {
return new ThreadLocal.ThreadLocalMap(parentMap); //这里就开始初始化TLMap对象了
}
// 根据parentMap来进行初始化子线程的TLMap对象
private ThreadLocalMap(ThreadLocal.ThreadLocalMap parentMap) {
ThreadLocal.ThreadLocalMap.Entry[] parentTable = parentMap.table; //拿到父线程里的哈希表
int len = parentTable.length;
setThreshold(len); // 设置阈值(具体方法参考上一篇)
table = new ThreadLocal.ThreadLocalMap.Entry[len];
for (int j = 0; j < len; j++) {
ThreadLocal.ThreadLocalMap.Entry e = parentTable[j]; //将父线程里的Entry取出
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); //获取key
if (key != null) {
Object value = key.childValue(e.value); //获取value
ThreadLocal.ThreadLocalMap.Entry c = new ThreadLocal.ThreadLocalMap.Entry(key, value); //根据k-v重新生成一个Entry
int h = key.threadLocalHashCode & (len - 1); //计算哈希值
while (table[h] != null)
h = nextIndex(h, len); //线性探查解决哈希冲突问题(具体方法参考上一篇)
table[h] = c; //找到合适的位置后进行赋值
size++;
}
}
}
}
// ITL里的childValue的实现
protected T childValue(T parentValue) {
return parentValue; //直接将父线程里的值返回
}
3、ITL所带来的的问题
看过上述代码后,现在关于ITL的实现我们基本上有了清晰的认识了,根据其实现性质,可以总结出在使用ITL时可能存在的问题:
3.1:线程不安全
写在前面:这里讨论的线程不安全对象不包含Integer等类型,因为这种对象被重新赋值,变掉的是整个引用,这里说的是那种不改变对象引用,直接可以修改其内容的对象(典型的就是自定义对象的set方法)
如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量(本质上是同一个对象),参考上面的第三个例子,子线程写入后会覆盖掉主线程的变量,也是通过这个结果,我们确认了子线程TLMap里变量指向的对象和父线程是同一个。
3.2:线程池中可能失效
按照上述实现,在使用线程池的时候,ITL会完全失效,因为父线程的TLMap是通过init一个Thread的时候进行赋值给子线程的,而线程池在执行异步任务时可能不再需要创建新的线程了,因此也就不会再传递父线程的TLMap给子线程了。
针对上述2,我们来做个实验,来证明下猜想:
// 为了方便观察,我们假定线程池里只有一个线程
private static ExecutorService executorService = Executors.newFixedThreadPool(1);
private static ThreadLocal tl = new InheritableThreadLocal<>();
public static void main(String[] args) {
tl.set(1);
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
executorService.execute(()->{
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
});
executorService.execute(()->{
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
});
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
}
输出结果为:
线程名称-main, 变量值=1
线程名称-pool-1-thread-1, 变量值=1
线程名称-main, 变量值=1
线程名称-pool-1-thread-1, 变量值=1
会发现,并没有什么问题,和我们预想的并不一样,原因是什么呢?因为线程池本身存在一个初始化的过程,第一次使用的时候发现里面的线程数(worker数)少于核心线程数时,会进行创建线程,既然是创建线程,一定会执行Thread的init方法,参考上面提到的源码,在第一次启用线程池的时候,类似做了一次new Thread的操作,因此是没有什么问题的,父线程的TLMap依然可以传递下去。
现在我们改造下代码,把tl.set(1)改到第一次启用线程池的下面一行,然后再看看:
public static void main(String[] args) throws Exception{
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
executorService.execute(()->{
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
});
tl.set(1); // 等上面的线程池第一次启用完了,父线程再给自己赋值
executorService.execute(()->{
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
});
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
}
输出结果为:
线程名称-main, 变量值=null
线程名称-main, 变量值=1
线程名称-pool-1-thread-1, 变量值=null
线程名称-pool-1-thread-1, 变量值=null
很明显,第一次启用时没有递进去的值,在后续的子线程启动时就再也传递不进去了。
但是,在实际项目中我们大多数采用线程池进行做异步任务,假如真的需要传递主线程的本地变量,使用ITL的问题显然是很大的,因为是有极大可能性拿不到任何值的,显然在实际项目中,ITL的位置实在是尴尬,所以在启用线程池的情况下,不建议使用ITL做值传递。为了解决这种问题,阿里做了transmittable-thread-local(TTL)来解决线程池异步值传递问题
三、TransmittableThreadLocal的使用及原理解析
1、基本使用
首先,TTL是用来解决ITL解决不了的问题而诞生的,所以TTL一定是支持父线程的本地变量传递给子线程这种基本操作的,ITL也可以做到,但是前面有讲过,ITL在线程池的模式下,就没办法再正确传递了,所以TTL做出的改进就是即便是在线程池模式下,也可以很好的将父线程本地变量传递下去,先来看个例子
// 需要注意的是,使用TTL的时候,要想传递的值不出问题,线程池必须得用TTL加一层代理(下面会讲这样做的目的)
private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
private static ThreadLocal tl = new TransmittableThreadLocal<>(); //这里采用TTL的实现
public static void main(String[] args) {
new Thread(() -> {
String mainThreadName = "main_01";
tl.set(1);
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
sleep(1L); //确保上面的会在tl.set执行之前执行
tl.set(2); // 等上面的线程池第一次启用完了,父线程再给自己赋值
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
String mainThreadName = "main_02";
tl.set(3);
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
sleep(1L); //确保上面的会在tl.set执行之前执行
tl.set(4); // 等上面的线程池第一次启用完了,父线程再给自己赋值
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
executorService.execute(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
});
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
}).start();
}
private static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
运行结果:
线程名称-Thread-2, 变量值=4
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-pool-1-thread-1, 变量值=3
线程名称-Thread-1, 变量值=2
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-pool-1-thread-2, 变量值=1
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-pool-1-thread-1, 变量值=1
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-pool-1-thread-2, 变量值=3
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-pool-1-thread-2, 变量值=3
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-pool-1-thread-1, 变量值=1
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-pool-1-thread-2, 变量值=2
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-pool-1-thread-1, 变量值=4
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-pool-1-thread-1, 变量值=4
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-pool-1-thread-2, 变量值=4
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-pool-1-thread-1, 变量值=2
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-pool-1-thread-2, 变量值=2
程序有些啰嗦,为了说明问题,加了很多说明,但至少通过上面的例子,不难发现,两个主线程里都使用线程池异步,而且值在主线程里还发生过改变,测试结果展示一切正常,由此可以知道TTL在使用线程池的情况下,也可以很好的完成传递,而且不会发生错乱。
那么是不是对普通线程异步也有这么好的支撑呢?
改造下上面的测试代码:
private static ThreadLocal tl = new TransmittableThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
String mainThreadName = "main_01";
tl.set(1);
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
sleep(1L); //确保上面的会在tl.set执行之前执行
tl.set(2); // 等上面的线程池第一次启用完了,父线程再给自己赋值
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
String mainThreadName = "main_02";
tl.set(3);
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
sleep(1L); //确保上面的会在tl.set执行之前执行
tl.set(4); // 等上面的线程池第一次启用完了,父线程再给自己赋值
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
new Thread(() -> {
sleep(1L);
System.out.println(String.format("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get()));
}).start();
System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));
}).start();
}
相比第一段测试代码,这一段的异步全都是普通异步,未采用线程池的方式进行异步,看下运行结果:
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-Thread-14, 变量值=4
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-Thread-5, 变量值=1
线程名称-Thread-1, 变量值=2
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-Thread-3, 变量值=1
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-Thread-11, 变量值=2
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-Thread-6, 变量值=3
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-Thread-12, 变量值=4
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-Thread-10, 变量值=4
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-Thread-8, 变量值=3
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-Thread-4, 变量值=3
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-Thread-7, 变量值=1
线程名称-Thread-2, 变量值=4
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-Thread-9, 变量值=2
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-Thread-13, 变量值=2
ok,可以看到,达到了跟第一个测试一致的结果。
到这里,通过上述两个例子,TTL的基本使用,以及其解决的问题,我们已经有了初步的了解,下面我们来解析一下其内部原理,看看TTL是怎么完成对ITL的优化的。
2、原理分析
先来看TTL里面的几个重要属性及方法
TTL定义:
public class TransmittableThreadLocal extends InheritableThreadLocal
可以看到,TTL继承了ITL,意味着TTL首先具备ITL的功能。
再来看看一个重要属性holder:
/**
* 这是一个ITL类型的对象,持有一个全局的WeakMap(weakMap的key是弱引用,同TL一样,也是为了解决内存泄漏的问题),里面存放了TTL对象
* 并且重写了initialValue和childValue方法,尤其是childValue,可以看到在即将异步时父线程的属性是直接作为初始化值赋值给子线程的本地变量对象(TTL)的
*/
private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =
new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
@Override
protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
return new WeakHashMap<TransmittableThreadLocal<?>, Object>();
}
@Override
protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);
}
};
再来看下set和get:
//下面的方法均属于TTL类
@Override
public final void set(T value) {
super.set(value);
if (null == value) removeValue();
else addValue();
}
@Override
public final T get() {
T value = super.get();
if (null != value) addValue();
return value;
}
private void removeValue() {
holder.get().remove(this); //从holder持有的map对象中移除
}
private void addValue() {
if (!holder.get().containsKey(this)) {
holder.get().put(this, null); //从holder持有的map对象中添加
}
}
TTL里先了解上述的几个方法及对象,可以看出,单纯的使用TTL是达不到支持线程池本地变量的传递的,通过第一部分的例子,可以发现,除了要启用TTL,还需要通过TtlExecutors.getTtlExecutorService
包装一下线程池才可以,那么,下面就来看看在程序即将通过线程池异步的时候,TTL帮我们做了哪些操作(这一部分是TTL支持线程池传递的核心部分):
首先打开包装类,看下execute方法在执行时做了些什么。
// 此方法属于线程池包装类ExecutorTtlWrapper
@Override
public void execute(@Nonnull Runnable command) {
executor.execute(TtlRunnable.get(command)); //这里会把Rannable包装一层,这是关键,有些逻辑处理,需要在run之前执行
}
// 对应上面的get方法,返回一个TtlRunnable对象,属于TtLRannable包装类
@Nullable
public static TtlRunnable get(@Nullable Runnable runnable) {
return get(runnable, false, false);
}
// 对应上面的get方法
@Nullable
public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
if (null == runnable) return null;
if (runnable instanceof TtlEnhanced) { // 若发现已经是目标类型了(说明已经被包装过了)直接返回
// avoid redundant decoration, and ensure idempotency
if (idempotent) return (TtlRunnable) runnable;
else throw new IllegalStateException("Already TtlRunnable!");
}
return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun); //最终初始化
}
// 对应上面的TtlRunnable方法
private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
this.capturedRef = new AtomicReference<Object>(capture()); //这里将捕获后的父线程本地变量存储在当前对象的capturedRef里
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
// 对应上面的capture方法,用于捕获当前线程(父线程)里的本地变量,此方法属于TTL的静态内部类Transmitter
@Nonnull
public static Object capture() {
Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) { // holder里目前存放的k-v里的key,就是需要传给子线程的TTL对象
captured.put(threadLocal, threadLocal.copyValue());
}
return captured; // 这里返回的这个对象,就是当前将要使用线程池异步出来的子线程,所继承的本地变量合集
}
// 对应上面的copyValue,简单的将TTL对象里的值返回(结合之前的源码可以知道get方法其实就是获取当前线程(父线程)里的值,调用super.get方法)
private T copyValue() {
return copy(get());
}
protected T copy(T parentValue) {
return parentValue;
}
结合上述代码,大致知道了在线程池异步之前需要做的事情,其实就是把当前父线程里的本地变量取出来,然后赋值给Rannable包装类里的capturedRef
属性,到此为止,下面会发生什么,我们大致上可以猜出来了,接下来大概率会在run方法里,将这些捕获到的值赋给子线程的holder赋对应的TTL值,那么我们继续往下看Rannable包装类里的run方法是怎么实现的:
//run方法属于Rannable的包装类TtlRunnable
@Override
public void run() {
Object captured = capturedRef.get(); // 获取由之前捕获到的父线程变量集
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
/**
* 重点方法replay,此方法用来给当前子线程赋本地变量,返回的backup是此子线程原来就有的本地变量值(原生本地变量,下面会详细讲),
* backup用于恢复数据(如果任务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量属性恢复)
*/
Object backup = replay(captured);
try {
runnable.run(); // 执行异步逻辑
} finally {
restore(backup); // 结合上面对于replay的解释,不难理解,这个方法就是用来恢复原有值的
}
}
根据上述代码,我们看到了TTL在异步任务执行前,会先进行赋值操作(就是拿着异步发生时捕获到的父线程的本地变量,赋给自己),当任务执行完,就恢复原生的自己本身的线程变量值。
下面来具体看这俩方法:
//下面的方法均属于TTL的静态内部类Transmittable
@Nonnull
public static Object replay(@Nonnull Object captured) {
@SuppressWarnings("unchecked")
Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured; //使用此线程异步时捕获到的父线程里的本地变量值
Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>(); //当前线程原生的本地变量,用于使用完线程后恢复用
//注意:这里循环的是当前子线程原生的本地变量集合,与本方法相反,restore方法里循环这个holder是指:该线程运行期间产生的变量+父线程继承来的变量
for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
iterator.hasNext(); ) {
Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
TransmittableThreadLocal<?> threadLocal = next.getKey();
backup.put(threadLocal, threadLocal.get()); // 所有原生的本地变量都暂时存储在backup里,用于之后恢复用
/**
* 检查,如果捕获到的线程变量里,不包含当前原生变量值,则从当前原生变量里清除掉,对应的线程本地变量也清掉
* 这就是为什么会将原生变量保存在backup里的原因,为了恢复原生值使用
* 那么,为什么这里要清除掉呢?因为从使用这个子线程做异步那里,捕获到的本地变量并不包含原生的变量,当前线程
* 在做任务时的首要目标,是将父线程里的变量完全传递给任务,如果不清除这个子线程原生的本地变量,
* 意味着很可能会影响到任务里取值的准确性。
*
* 打个比方,有ttl对象tl,这个tl在线程池的某个子线程里存在对应的值2,当某个主线程使用该子线程做异步任务时
* tl这个对象在当前主线程里没有值,那么如果不进行下面这一步的操作,那么在使用该子线程做的任务里就可以通过
* 该tl对象取到值2,不符合预期
*/
if (!capturedMap.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// 这一步就是直接把父线程本地变量赋值给当前线程了(这一步起就刷新了holder里的值了,具体往下看该方法,在异步线程运行期间,还可能产生别的本地变量,比如在真正的run方法内的业务代码,再用一个tl对象设置一个值)
setTtlValuesTo(capturedMap);
// 这个方法属于扩展方法,ttl本身支持重写异步任务执行前后的操作,这里不再具体赘述
doExecuteCallback(true);
return backup;
}
// 结合之前Rannable包装类的run方法来看,这个方法就是使用上面replay记录下的原生线程变量做恢复用的
public static void restore(@Nonnull Object backup) {
@SuppressWarnings("unchecked")
Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup;
// call afterExecute callback
doExecuteCallback(false);
// 注意,这里的holder取出来的,实际上是replay方法设置进去的关于父线程里的所有变量(结合上面来看,就是:该线程运行期间产生的变量+父线程继承来的变量)
for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
iterator.hasNext(); ) {
Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
TransmittableThreadLocal<?> threadLocal = next.getKey();
/**
* 同样的,如果子线程原生变量不包含某个父线程传来的对象,那么就删除,可以思考下,这里的清除跟上面replay里的有什么不同?
* 这里会把不属于原生变量的对象给删除掉(这里被删除掉的可能是父线程继承下来的,也可能是异步任务在执行时产生的新值)
*/
if (!backupMap.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// 同样调用这个方法,进行值的恢复
setTtlValuesTo(backupMap);
}
// 真正给当前子线程赋值的方法,对应上面的setTtlValuesTo方法
private static void setTtlValuesTo(@Nonnull Map<TransmittableThreadLocal<?>, Object> ttlValues) {
for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet()) {
@SuppressWarnings("unchecked")
TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
threadLocal.set(entry.getValue()); //赋值,注意,从这里开始,子线程的holder里的值会被重新赋值刷新,可以参照上面ttl的set方法的实现
}
}
ok,到这里基本上把TTL比较核心的代码看完了,下面整理下整个流程,这是官方给出的时序图:
上图第一行指的是类名称,下面的流程指的是类所做的事情,根据上面罗列出来的源码,结合这个时序图,可以比较直观一些的理解整个流程。
3、TTL中线程池子线程原生变量的产生
这一节是为了验证上面replay和restore,现在通过一个例子来验证下,先把源码down下来,在源码的restore和replay上分别加上输出语句,遍历holder:
//replay前后打印holder里面的值
public static Object replay(@Nonnull Object captured) {
@SuppressWarnings("unchecked")
Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured;
Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>();
System.out.println("--------------------replay前置,当前拿到的holder里的TTL列表");
for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
iterator.hasNext(); ) {
Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
TransmittableThreadLocal<?> threadLocal = next.getKey();
System.out.println(String.format("replay前置里拿到原生的ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get()));
}
for...//代码省略,具体看上面
setTtlValuesTo(capturedMap);
doExecuteCallback(true);
System.out.println("--------------------reply后置,当前拿到的holder里的TTL列表");
for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
iterator.hasNext(); ) {
Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
TransmittableThreadLocal<?> threadLocal = next.getKey();
System.out.println(String.format("replay后置里拿到原生的ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get()));
}
return backup;
}
//restore前后打印holder里面的值
public static void restore(@Nonnull Object backup) {
@SuppressWarnings("unchecked")
Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup;
// call afterExecute callback
doExecuteCallback(false);
System.out.println("--------------------restore前置,当前拿到的holder里的TTL列表");
for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
iterator.hasNext(); ) {
Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
TransmittableThreadLocal<?> threadLocal = next.getKey();
System.out.println(String.format("restore前置里拿到当前线程内变量,ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get()));
}
for...//省略代码,具体具体看上面
setTtlValuesTo(backupMap);
System.out.println("--------------------restore后置,当前拿到的holder里的TTL列表");
for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
iterator.hasNext(); ) {
Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
TransmittableThreadLocal<?> threadLocal = next.getKey();
System.out.println(String.format("restore后置里拿到当前线程内变量,ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get()));
}
}
代码这样做的目的,就是要说明线程池所谓的原生本地变量是怎么产生的,以及replay和restore是怎么设置和恢复的,下面来看个简单的例子:
private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
private static ThreadLocal tl = new TransmittableThreadLocal();
private static ThreadLocal tl2 = new TransmittableThreadLocal();
public static void main(String[] args) throws InterruptedException {
tl.set(1);
tl2.set(2);
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
运行结果如下:
--------------------replay前置,当前拿到的holder里的TTL列表
replay前置里拿到原生的ttl_k=1259475182, ttl_value=2
replay前置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------reply后置,当前拿到的holder里的TTL列表
replay后置里拿到原生的ttl_k=1259475182, ttl_value=2
replay后置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------restore前置,当前拿到的holder里的TTL列表
restore前置里拿到当前线程内变量,ttl_k=1259475182, ttl_value=2
restore前置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
--------------------restore后置,当前拿到的holder里的TTL列表
restore后置里拿到当前线程内变量,ttl_k=1259475182, ttl_value=2
restore后置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
我们会发现,原生值产生了,从异步开始,就确定了线程池里的线程具备了1和2的值,那么,再来改动下上面的测试代码:
public static void main(String[] args) throws InterruptedException {
tl.set(1);
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread.sleep(1000L);
tl2.set(2);//较第一次换下位置,换到第一次使用线程池后执行(这意味着下面这次异步不会再触发Thread的init方法了)
System.out.println("---------------------------------------------------------------------------------");
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
运行结果为:
--------------------replay前置,当前拿到的holder里的TTL列表
replay前置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------reply后置,当前拿到的holder里的TTL列表
replay后置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------restore前置,当前拿到的holder里的TTL列表
restore前置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
--------------------restore后置,当前拿到的holder里的TTL列表
restore后置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
---------------------------------------------------------------------------------
--------------------replay前置,当前拿到的holder里的TTL列表
replay前置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------reply后置,当前拿到的holder里的TTL列表
replay后置里拿到原生的ttl_k=1020371697, ttl_value=2
replay后置里拿到原生的ttl_k=929338653, ttl_value=1
--------------------restore前置,当前拿到的holder里的TTL列表
restore前置里拿到当前线程内变量,ttl_k=1020371697, ttl_value=2
restore前置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
--------------------restore后置,当前拿到的holder里的TTL列表
restore后置里拿到当前线程内变量,ttl_k=929338653, ttl_value=1
可以发现,第一次异步时,只有一个值被传递了下去,然后第二次异步,新加了一个tl2的值,但是看第二次异步的打印,会发现,restore恢复后,仍然是第一次异步发生时放进去的那个tl的值。
通过上面的例子,基本可以确认,所谓线程池内线程的本地原生变量,其实是第一次使用线程时被传递进去的值,我们之前有说过TTL是继承至ITL的,之前的文章也说过,线程池第一次启用时是会触发Thread的init方法的,也就是说,在第一次异步时拿到的主线程的变量会被传递给子线程,作为子线程的原生本地变量保存起来,后续是replay操作和restore操作也是围绕着这个原生变量(即原生holder里的值)来进行设置、恢复的,设置的是当前父线程捕获到的本地变量,恢复的是子线程原生本地变量。
holder里持有的可以理解就是当前线程内的所有本地变量,当子线程将异步任务执行完毕后,会执行restore进行恢复原生本地变量,具体参照上面的代码和测试代码。
四、总结
到这里基本上确认了TTL是如何进行线程池传值的,以及被包装的run方法执行异步任务之前,会使用replay进行设置父线程里的本地变量给当前子线程,任务执行完毕,会调用restore恢复该子线程原生的本地变量(目前原生本地变量的产生,就只碰到上述测试代码中的这一种情况,即线程第一次使用时通过ITL属性以及Thread的init方法传给子线程,还不太清楚有没有其他方式设置)。
其实,正常程序里想要完成线程池上下文传递,使用TL就足够了,我们可以效仿TTL包装线程池对象的原理,进行值传递,异步任务结束后,再remove,以此类推来完成线程池值传递,不过这种方式过于单纯,且要求上下文为只读对象,否则子线程存在写操作,就会发生上下文污染。
TTL项目地址(可以详细了解下它的其他特性和用法):https://github.com/alibaba/transmittable-thread-local