ThreadLocal垮线程池传递数据解决方案:TransmittableThreadLocal【享学Java】
企业不是慈善机构:创造利润是你存在的核心价值。
目录
- 前言
- 正文
- InheritableThreadLocal的局限性
- Thread#init方法探究
- TransmittableThreadLocal
- 如何实现?
- 使用场景
- 总结
- 关注A哥
前言
在 上篇文章 了解到了,ThreadLocal
它并不能解决线程安全问题,它旨在用于传递数据。但是它能成功传递数据比如有个大前提:放数据和取数据的操作必须是处于相同线程。
即使JDK扩展出了一个子类:InheritableThreadLocal
,它能够支持跨线程传递数据,但也仅限于父线程给子线程来传递数据。倘若两个线程间真的八竿子打不着,比如分别位于两个线程池内的线程,它们之间要传递数据该肿么办呢?这就是跨线程池之间的数据传递范畴,是本文将要讲解的主要内容。
正文
在实际生产中,线程一般不可能孤立的独立去运行,而是交给线程池去调度处理。所以实际上几乎没有纯正的父子线程的关系存在,而若有这种需求大多是线程池与线程池之间的线程联系。
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初始化的时候,才会去同步一份最新数据过来。
对于这两个示例的结果可做如下解释:
- 示例1的线程池大小是1,所以第二个线程执行时复用的是上个线程(你看线程名称都一样),所以就不会再经历init初始化阶段,所以得到的绑定数据还是旧数据
- 示例2的线程池大小是2,所以第二个线程执行时会继续初始化一条新的线程来执行它,会触发到init过程,所以它获取到的是最新绑定的数据。
小提示:线程池内线程数量若还没达到coreSize大小的话,每次新任务都会启用新的线程来执行的(不管是否有空闲线程与否)
Thread#init方法探究
为了理解后面方案的实现,非常有必要对线程初始化方法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
),那么就会把父线程绑定的变量们 拷贝一份到子线程里- 拷贝的原理类似于Map复制,只不过其在Hash冲突时,不是使用链表结构,而是直接在数组中找下一个为null的槽位放里面
说明:这里的拷贝是浅拷贝:引用传递而已。如果想要深度拷贝,需要自行复写
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
。
TransmittableThreadLocal
TTL是阿里巴巴开源的专门解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。
TransmittableThreadLocal
简称TTL,InheritableThreadLocal
简称ITL
它的官网是:https://github.com/alibaba/transmittable-thread-local
功能介绍我已截图至此:
GAV:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.11.4</version>
</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完全零侵入方式接入,可以说是非常强大和好用的一个基础工具,值得使用明白,对中间件团队能提供良好的支持。
使用场景
官方流出了其四大使用场景:
- 分布式跟踪系统(链路追踪)
- 日志收集记录系统上下文(MDC)
- Session级Cache
- 应用容器或上层框架跨应用代码给下层SDK传递信息
其中场景1和场景2在全链路压测平台打造的时候都会触及到,所以基于TTL来解决这些问题不失外一个非常好的选择。
总结
ThreadLocal
的一步步的进化,最终来到了TransmittableThreadLocal
,它能够满足我们对线程间数据传递的几乎一切遐想,这对我们做类似于全链路压测这种平台的时候非常有帮助,期待成效。
关注A哥
Author | A哥(YourBatman) |
---|---|
个人站点 | www.yourbatman.cn |
yourbatman@qq.com | |
微 信 | fsx641385712 |
活跃平台 | ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
公众号 | BAT的乌托邦(ID:BAT-utopia) |
知识星球 | BAT的乌托邦 |
每日文章推荐 | 每日文章推荐 |
- ThreadLocal能解决线程安全问题?胡扯!本文教你正确的使用姿势【享学Java】
- [享学Jackson] 一、初识Jackson – 世界上最好的JSON库
- [享学Jackson] 二、jackson-core之流式API与JsonFactory、JsonGenerator、JsonParser
- [享学Jackson] 三、jackson-databind之ObjectMapper与数据绑定、树模型
- [享学Jackson] 四、控制Jackson行为的特征们之JsonFactory.Feature、JsonGenerator.Feature、JsonParser.Feature
- [享学Jackson] 五、控制Jackson行为的特征们之JsonWriteFeature、JsonReadFeature
- [享学Jackson] 六、控制Jackson行为的特征们之MapperFeature、SerializationFeature、DeserializationFeature
- [享学Jackson] 七、Jackson使用bit位运算来开启/禁用Feature的原理解析
- [享学Jackson] 八、jackson-databind数据绑定基础配置之BaseSettings、MapperConfig、MapperConfigBase
- [享学Jackson] 九、jackson-databind数据绑定序列化/反序列化配置之SerializationConfig、DeserializationConfig
- [享学Jackson] 十、jackson-databind序列化之ObjectMapper序列化原理、序列化器匹配原理
- [享学Jackson] 十一、jackson-databind之JsonSerializer序列化器全解析
- [享学Jackson] 十二、jackson-databind反序列化之ObjectMapper反序列化原理、JsonDeserializer反序列化器全解析
- [享学Jackson] 十三、jackson-annotation注解模块全解析及Jackson注解大全
- [享学Jackson] 十四、深入理解Jackson的Module模块化设计及原理分析
- [享学Jackson] 十五、第三方模块Module的深度实践:JavaTimeModule、JSR310Module、ParameterNamesModule、Jdk8Module
- [享学Jackson] 十六、Jackson在Spring MVC中的使用之Date、JSR310时间类型的处理
- [享学Jackson] 十七、spring-web整合Jackson源码解析之Jackson2ObjectMapperBuilder
- [享学Jackson] 十八、Spring容器深度整合Jackson的桥梁之SpringHandlerInstantiator
- [享学Jackson] 十九、Spring下使用ObjectMapper的正确姿势 — Jackson2ObjectMapperFactoryBean
- [享学Jackson] 二十、Spring MVC下的Jackson — MappingJackson2HttpMessageConverter
- [享学Jackson] 二十一、Spring Boot下的Jackson — JacksonAutoConfiguration自动配置
- [享学Jackson] 二十二、Jackson与Fastjson的恩怨情仇(完结篇)
还没有评论,来说两句吧...