企业不是慈善机构:创造利润是你存在的核心价值。
在 上篇文章 了解到了,ThreadLocal
它并不能解决线程安全问题,它旨在用于传递数据。但是它能成功传递数据比如有个大前提:放数据和取数据的操作必须是处于相同线程。
即使JDK扩展出了一个子类:InheritableThreadLocal
,它能够支持跨线程传递数据,但也仅限于父线程给子线程来传递数据。倘若两个线程间真的八竿子打不着,比如分别位于两个线程池内的线程,它们之间要传递数据该肿么办呢?这就是跨线程池之间的数据传递范畴,是本文将要讲解的主要内容。
在实际生产中,线程一般不可能孤立的独立去运行,而是交给线程池去调度处理。所以实际上几乎没有纯正的父子线程的关系存在,而若有这种需求大多是线程池与线程池之间的线程联系。
上篇文章 介绍了ThreadLocal
的局限性,可以使用更强的子类InheritableThreadLocal
予以解决。那么这里看看如下示例:
public class TestThreadLocal {
private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();
private static final ExecutorService THREAD_POOL = Executors.newSingleThreadExecutor();
@Test
public void fun1() throws InterruptedException {
THREAD_LOCAL.set(new Person());
THREAD_POOL.execute(() -> getAndPrintData());
TimeUnit.SECONDS.sleep(2);
Person newPerson = new Person();
newPerson.setAge(100);
THREAD_LOCAL.set(newPerson); // 给线程重新绑定值
THREAD_POOL.execute(() -> getAndPrintData());
TimeUnit.SECONDS.sleep(2);
}
private void setData(Person person) {
System.out.println("set数据,线程名:" + Thread.currentThread().getName());
THREAD_LOCAL.set(person);
}
private Person getAndPrintData() {
Person person = THREAD_LOCAL.get();
System.out.println("get数据,线程名:" + Thread.currentThread().getName() + ",数据为:" + person);
return person;
}
@Setter
@ToString
private static class Person {
private Integer age = 18;
}
}
运行程序,控制台打印:
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
重新绑定竟然“未生效”?在原基础上什么都不动,仅仅只改变线程池的大小:
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(2);
再次运行程序,控制台打印:
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-2,数据为:TestThreadLocal.Person(age=100)
这个结果能接受且符合预期。可以看到线程名是不一样的,所以第二个线程获取到了最新绑定的结果。因此可以大胆猜测:线程在init初始化的时候,才会去同步一份最新数据过来。
对于这两个示例的结果可做如下解释:
小提示:线程池内线程数量若还没达到coreSize大小的话,每次新任务都会启用新的线程来执行的(不管是否有空闲线程与否)
为了理解后面方案的实现,非常有必要对线程初始化方法Thread#init
理解一番。
Thread#init:
// inheritThreadLocals是否继承线程的本地变量们(默认是true)
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
Thread parent = currentThread();
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
/* Set thread ID */ // 给线程一个自增的id
tid = nextThreadID();
}
子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init
方法在Thread的构造方法中被调用。
从摘录出来的源码出能得到如下重点:
inheritableThreadLocals != null
)并且允许继承(inheritThreadLocals = true
),那么就会把父线程绑定的变量们 拷贝一份到子线程里
说明:这里的拷贝是浅拷贝:引用传递而已。如果想要深度拷贝,需要自行复写
ThreadLocal#childValue()
方法(比如你可以继承InheritableThreadLocal
并重写childValue方法)
那么为何ThreadLocal
不具备继承性,而InheritableThreadLocal
可以呢?有了上面的知识储备,现在一探其源码便知:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 现在知道为何是浅拷贝了吧~~~~~~
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
// 只要inheritableThreadLocals不为null了,那可不就完成子线程可以继承父的了吗
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
源码不会骗人,一切都透露得明明白白的了吧。
InheritableThreadLocal
支持子线程访问父线程中本地变量的原理是:创建子线程时将父线程中的本地变量值拷贝了一份到自己这来,拷贝的时机是子线程创建时。
然后在实际开发中,多线程就离不开线程池的使用,因为线程池能够复用线程,减少线程的频繁创建与销毁。倘若合格时候使用InheritableThreadLocal
来传递数据,那么线程池中的线程拷贝的数据始终来自于第一个提交任务的外部线程,这样非常容易造成线程本地变量混乱,这种错误是致命的,比如示例1就是这种例子~
那么,这种问题怎么破?JDK并没有提供源生的支持,这时候就得借助阿里巴巴开源的TTL(transmittable-thread-local
):TransmittableThreadLocal
。
TTL是阿里巴巴开源的专门解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。
TransmittableThreadLocal
简称TTL,InheritableThreadLocal
简称ITL
它的官网是:https://github.com/alibaba/transmittable-thread-local
功能介绍我已截图至此:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>transmittable-thread-localartifactId>
<version>2.11.4version>
dependency>
那么使用它就能解决如上示例的问题吗?正所谓试验是检验真理的唯一标准,来一把:
针对示例1,仅仅做出如下改动(其它均不变):
// 实现类使用TTL的实现
private static final ThreadLocal<Person> THREAD_LOCAL = new TransmittableThreadLocal<>();
// 线程池使用TTL包装一把
private static final ExecutorService THREAD_POOL = TtlExecutors.getTtlExecutorService(Executors.newSingleThreadExecutor());
再次运行程序,控制台打印:
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=100)
bingo!看线程名仍旧还是同一个线程(因为线程池大小为1嘛),但是结果已经是最新的了,这才是合理的嘛,不禁想感叹一句:太它xxxxx了!
说明:这里线程池必须使用
TtlExecutors
处理一下,而且得使用TransmittableThreadLocal
作为数据传递的实现,缺一不可哦~
TransmittableThreadLocal
继承于InheritableThreadLocal
,并拥有了 InheritableThreadLocal
对子线程传递上下文的特性,只需解决线程池上下文传递问题。它使用TtlRunnable
包装了任务的运行,被包装的run方法执行异步任务之前,会使用replay进行设置父线程里的本地变量给当前子线程,任务执行完毕,会调用restore恢复该子线程原生的本地变量,当然重点还是稍显复杂的上下文管理部分。
本文并不涉及到它详细的原理,建议有兴趣者可以上它官网看看(不算很复杂),全中文的也好理解,并且还附有其执行时序图。
说明:它还支持javaagent完全零侵入方式接入,可以说是非常强大和好用的一个基础工具,值得使用明白,对中间件团队能提供良好的支持。
官方流出了其四大使用场景:
其中场景1和场景2在全链路压测平台打造的时候都会触及到,所以基于TTL来解决这些问题不失外一个非常好的选择。
ThreadLocal
的一步步的进化,最终来到了TransmittableThreadLocal
,它能够满足我们对线程间数据传递的几乎一切遐想,这对我们做类似于全链路压测这种平台的时候非常有帮助,期待成效。
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。