ThreadLocal的进化——TransmittableThreadLocal
上一篇文章中,我们谈到了 InheritableThreadLocal,它解决了 ThreadLocal 针对父子线程无法共享上下文的问题。但我们可能听说过阿里的开源产品TransmittableThreadLocal,那么它又是做什么的呢?
线程池中的共享
我们在多线程中,很少会直接 new 一个线程,更多的可能是利用线程池处理任务,那么利用 InheritableThreadLocal 可以将生成任务线程的上下文传递给执行任务的线程吗?废话不多说,直接上代码测试一下:
public class InheritableThreadLocalContext {
private static InheritableThreadLocal<Context> context = new InheritableThreadLocal<>();
static class Context {
String name;
int value;
}
public static void main(String[] args) {
// 固定线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(
() -> {
// 生成任务的线程对context进行赋值
Context contextMain = new Context();
contextMain.name = String.format("Thread%s name", finalI);
contextMain.value = finalI * 20;
InheritableThreadLocalContext.context.set(contextMain);
// 提交任务
for (int j = 1; j <= 10; j++) {
System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
executorService.execute(() -> {
// 执行任务的子线程
Context contextChild = InheritableThreadLocalContext.context.get();
System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
});
}
}
).start();
}
}
}
我们希望的结果是,子线程输出的内容能够和父线程对应上。然而,实际的结果却出乎所料,我将结果整理一下:
Thread1 produce task 21
// 省略8行
Thread1 produce task 30
Thread2 produce task 41
// 省略8行
Thread2 produce task 50
pool-1-thread-1 execute task, name : Thread2 name value : 40
// 省略47行
pool-1-thread-1 execute task, name : Thread2 name value : 40
Thread3 produce task 61
// 省略8行
Thread3 produce task 70
Thread4 produce task 81
// 省略8行
Thread4 produce task 90
Thread5 produce task 101
// 省略8行
Thread5 produce task 110
Thread6 produce task 121
// 省略8行
Thread6 produce task 130
Thread7 produce task 141
// 省略8行
Thread7 produce task 150
pool-1-thread-2 execute task, name : Thread7 name value : 140
// 省略6行
pool-1-thread-2 execute task, name : Thread7 name value : 140
Thread8 produce task 161
// 省略8行
Thread8 produce task 170
Thread9 produce task 181
// 省略8行
Thread9 produce task 190
pool-1-thread-4 execute task, name : Thread9 name value : 180
pool-1-thread-4 execute task, name : Thread9 name value : 180
Thread10 produce task 201
// 省略8行
Thread10 produce task 210
pool-1-thread-3 execute task, name : Thread10 name value : 200
// 省略39行
pool-1-thread-3 execute task, name : Thread10 name value : 200
虽然生产总数和消费总数都是100,但是明显有的消费多了,有的消费少了。合理推测一下,应该是在主线程放进任务后,子线程才生成。为了验证这个猜想,将线程池用 ThreadPoolExecutor 生成,并在用子线程生成任务之前,先赋值 context 并开启所有线程:
public static void main(String[] args) {
// 固定线程池
ThreadPoolExecutor executorService = new ThreadPoolExecutor(
4,
4,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>() );
// 在main线程中赋值
Context context = new Context();
context.name = "Thread0 name";
context.value = 0;
InheritableThreadLocalContext.context.set(context);
// 开启所有线程
executorService.prestartAllCoreThreads();
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(
() -> {
// 生成任务的线程对context进行赋值
Context contextMain = new Context();
contextMain.name = String.format("Thread%s name", finalI);
contextMain.value = finalI * 20;
InheritableThreadLocalContext.context.set(contextMain);
// 提交任务
for (int j = 1; j <= 10; j++) {
System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
executorService.execute(() -> {
// 执行任务的子线程
Context contextChild = InheritableThreadLocalContext.context.get();
System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
});
}
}
).start();
}
}
结果不出所料,执行任务的线程输出的,都是最外面主线程设置的值。
那么我们该如何才能达到最初想要的效果呢?就是利用线程池执行任务时,如何能够让执行者线程能够获取调用者线程的 context 呢?
使用 TransmittableThreadLocal 解决
上面的问题主要是因为执行任务的线程是被线程池管理,可以被复用(可以称为池化复用)。那复用了之后,如果还是依赖于父线程的 context,自然是有问题的,因为我们想要的效果是执行线程获取调用线程的 context,这时候就是TransmittableThreadLocal出场了。
TransmittableThreadLocal 是阿里提供的工具类,其主要解决的就是上面遇到的问题。那么该如何使用呢?
首先,你需要引入相应的依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.11.0</version>
</dependency>
具体代码,就拿上文提到的情况,我们用 TransmittableThreadLocal 做一个改造:
public class TransmittableThreadLocalTest {
private static TransmittableThreadLocal<Context> context = new TransmittableThreadLocal<>();
static class Context {
String name;
int value;
}
public static void main(String[] args) {
// 固定线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(
() -> {
// 生成任务的线程对context进行赋值
Context contextMain = new Context();
contextMain.name = String.format("Thread%s name", finalI);
contextMain.value = finalI * 20;
TransmittableThreadLocalTest.context.set(contextMain);
// 提交任务
for (int j = 1; j <= 10; j++) {
System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
Runnable task = () -> {
// 执行任务的子线程
Context contextChild = TransmittableThreadLocalTest.context.get();
System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
};
// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.execute(ttlRunnable);
}
}
).start();
}
}
}
此时再次运行,就会发现执行线程运行时的输出内容是完全可以和调用线程对应上的了。当然了,我这种方式是修改了 Runnable 的写法,阿里也提供了线程池的写法,简单如下:
public static void main(String[] args) {
// 固定线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);
ExecutorService finalExecutorService = executorService;
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(
() -> {
// 生成任务的线程对context进行赋值
Context contextMain = new Context();
contextMain.name = String.format("Thread%s name", finalI);
contextMain.value = finalI * 20;
TransmittableThreadLocalTest.context.set(contextMain);
// 提交任务
for (int j = 1; j <= 10; j++) {
System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
Runnable task = () -> {
// 执行任务的子线程
Context contextChild = TransmittableThreadLocalTest.context.get();
System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
};
finalExecutorService.execute(task);
}
}
).start();
}
}
其实还有更加简单的写法,具体可以参考其github:https://github.com/alibaba/transmittable-thread-local
总结
其实两篇 ThreadLocal 升级文章的出现,都是因为周三听了一个部门关于 TTL 的分享会,也是介绍了 TransmittableThreadLocal,但因为携程商旅面临国际化的改动,当前的语种信息肯定是存储在线程的 context 中最方便,但涉及到线程传递的问题(因为会调用异步接口等等),所以自然就需要考虑这个了。性能方面的话,他们有做过测试,但我也只是一个听者,并没有具体使用过,大家也可以一起交流。
有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。
公众号:健程之道


ThreadLocal的进化——TransmittableThreadLocal的更多相关文章
- ThreadLocal的进化——InheritableThreadLocal
之前有介绍过 ThreadLocal,JDK 后来针对此做了一个升级版本 InheritableThreadLocal,今天就来好好介绍下. 为什么要升级 首先我们来想想,为什么要升级?这就要说起 T ...
- ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析
ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析 上一篇:ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解 ...
- 【原理】Java的ThreadLocal实现原理浅读
当前线程的值传递,ThreadLocal 通过ThreadLocal设值,在线程内可获取,即时获取值时在其它Class或其它Method. public class BasicUsage { priv ...
- ThreadLocal (三):为何TransmittableThreadLocal
一.示例 线程池内的线程并没有父子关系,所以不适合InheritableThreadLocal的使用场景 public class ThreadPoolInheritableThreadLocalDe ...
- 使用 transmittable-thread-local 组件解决 ThreadLocal 父子线程数据传递问题
在某个项目中,需要使用mybatis-plus多租户功能以便数据隔离,前端将租户id传到后端,后端通过拦截器将该租户id设置到ThreadLocal以便后续使用,代码大体上如下所示: ThreadLo ...
- ThreadLocal父子线程传递实现方案
介绍InheritableThreadLocal之前,假设对 ThreadLocal 已经有了一定的理解,比如基本概念,原理,如果没有,可以参考:ThreadLocal源码分析解密.在讲解之前我们先列 ...
- TransmittableThreadLocal 解决 线程池线程复用 无法复制 InheritableThreadLocal 的问题.
ThreadLoacl,InheritableThreadLocal,原理,以及配合线程池使用的一些坑 TransmittableThreadLocal 原理 之前为了能让InheritableThr ...
- ThreadLocal剧集(一)
总述 最近做了一个日志调用链路跟踪的项目,涉及到操作标识在线程和子线程,线程池以及远程调用之间的传递问题.最终采用了阿里开源的TransmittableThreadLocal插件(https: ...
- ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解析
ThreadLocal系列之InheritableThreadLocal的使用及原理解析(源码基于java8) 上一篇:ThreadLocal系列(一)-ThreadLocal的使用及原理解析 下一篇 ...
随机推荐
- vue使用一些外部插件及样式的配置
一.配置全局css及js样式 1.首先将事先写好的css文件及js文件放在src文件目录下的assets文件下 2.在main.js文件输上图右边两个红色框的代码 二.配置全局jQuery及boots ...
- Dubbo的应用
导语:Dubbo是阿里巴巴的一个分布式服务的开源框架,致力于提供高性能和透明化的RPC远程服务调用方案,是阿里巴巴SOA服务化治理方案的核心框架,每天为2,000+个服务提供3,000,000,000 ...
- Java虚拟机-字节码指令
目录 字节码指令 字节码与数据类型 加载和存储指令 运算指令 类型转换指令 对象创建与访问指令 操作数栈管理指令 控制转移指令 方法调用和返回指令 异常处理指令 同步指令 字节码指令 Java虚拟机的 ...
- c#属性(Property)
属性(Property)是类(class).结构(structure)和接口(interface)的命名(named)成员.类或结构中的成员变量或方法称为 域(Field).属性(Property)是 ...
- Appium+python自动化(四十)-Appium自动化测试框架综合实践 - 代码实现(超详解)
1.简介 今天我们紧接着上一篇继续分享Appium自动化测试框架综合实践 - 代码实现.由于时间的关系,宏哥这里用代码给小伙伴演示两个模块:注册和登录. 2.业务模块封装 因为现在各种APP的层出不群 ...
- Python 常用模块系列学习(3)--configparser module
configpaser 模块----用于生成和修改常见配置文档 1. config 对象的创建: import configparser #导入模块 config = configparser.Con ...
- 一文教你快速读懂MQTT网关
MQTT是一种发布(publish)/订阅(subscribe)协议,MQTT协议采用发布/订阅模式,所有的物联网终端都通过TCP连接到云端,云端通过主题的方式管理各个设备关注的通讯内容,负责将设备与 ...
- 【评测机】评测时报错cc1plus: fatal error: /xx/xx/main.cpp: Permission denied compilation terminated.的解决方法
事情是这亚子发生的,原本建立评测机的时候就出现过这个问题,但莫名其妙就解决了. 报错的文件路径是位于docker内的,所以本质上这个错误是docker内的没有权限执行相关文件. 原因是centos7中 ...
- SpringBoot 源码解析 (九)----- Spring Boot的核心能力 - 整合Mybatis
本篇我们在SpringBoot中整合Mybatis这个orm框架,毕竟分析一下其自动配置的源码,我们先来回顾一下以前Spring中是如何整合Mybatis的,大家可以看看我这篇文章Mybaits 源码 ...
- 用Swoole+React 实现的聊天室
前后端分离的项目,使用 Swoole+React 实现的聊天室,整个项目的框架结构可以进行参考,前端 react+react-redux+react-router+react-ant 等等,后台使用 ...