TransmittableThreadLocal解决线程间上下文传递烦恼
作者:梦想实现家_Z 发布时间:2023-11-09 17:09:35
前言
在一些项目中,经常会遇到需要把当前线程中的上下文传递到其他线程中的情况,比如某项目包含国际化操作,在业务请求进来时需要把对应的国家代码存储到当前线程中,以便后续的业务逻辑能够根据国家代码正确地处理;另外在一些异步化操作中,也要保证异常线程中也能够正确地获取到对应的国家代码。
在上述业务场景中,我们很自然的就想到了使用ThreadLocal
,但是ThreadLocal
无法解决父子线程间上下文传递的问题,此时InheritableThreadLocal
站出来了,它在创建子线程的过程中
拷贝了父亲线程中的inheritableThreadLocals
数据,在new Thread()
代码中,有一段这样的代码:
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
但是在真实的项目当中,异步操作几乎都是用的线程池来处理,也就意味着线程是复用的,这就导致了不同任务的上下文使用的是同一个线程的上下文,这就会导致程序出现意料不到的BUG
。
针对这种情况,我们发现应该把线程上下文转变成任务上下文,这样的话才能避免多个任务共用一个线程上下文,为此我们不得不封装一下每一个传入线程池的任务:
class RunnableWrap implements Runnable {
private ThreadLocal threadLocal;
private Object context;
private Runnable task;
public RunnableWrap(ThreadLocal threadLocal, Runnable task) {
this.threadLocal = threadLocal;
this.context = threadLocal.get();
this.task = task;
}
@Override
public void run() {
try {
threadLocal.set(context);
task.run();
} finally {
threadLocal.remove();
}
}
}
但是这样做确实不是很优雅,所以为何不用TransmittableThreadLocal
试试呢?
示例
我们来通过一个示例演示一下TransmittableThreadLocal
是否能够在线程池中实现上下文的传递,并且满足任务间上下文的隔离效果:
private static TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>();
// 使用只有一个线程的线程池,测试线程复用是否影响TransmittableThreadLocal的效果
private static final Executor EXECUTOR = Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
// 设置主线程的上下文为"china"
CONTEXT.set("china");
// 创建第一个任务,通过TtlRunnable.get()包装;
// 在第一个任务中查看上下文数据,检查是否拿到正确的上下文;
// 另外再修改掉该上下文,主要测试是否会影响第二个任务的上下文;
Runnable task1 = TtlRunnable.get(() -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "开始");
String countryCode = CONTEXT.get();
System.out.println("第一个任务执行结果:" + countryCode);
// 修改该线程中上下文值,检查是否影响第二个任务
CONTEXT.set("US");
System.out.println(thread.getName() + "结束");
});
// 第二个任务主要测试上下文是否受第一个任务的影响
Runnable task2 = TtlRunnable.get(() -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "开始");
String countryCode = CONTEXT.get();
System.out.println("第二个任务执行结果:" + countryCode);
System.out.println(thread.getName() + "结束");
});
// 按顺序执行两个任务,全部放到线程池中执行
CompletableFuture.runAsync(task1, EXECUTOR1)
.thenRunAsync(task2, EXECUTOR1);
// 检查主线程上下文是否受影响;
String countryCode = CONTEXT.get();
System.out.println("主线程执行结果:" + countryCode);
Thread.sleep(10000);
}
1.我们准备了只有一个线程的线程池,主要测试线程复用的情况;
2.准备了两个任务,第一个任务检查是否能够拿到正确的上下文数据;第二个任务测试是否因为第一个任务修改上下文受到影响;
执行结果如下:
pool-1-thread-1开始
第一个任务执行结果:china
pool-1-thread-1结束
pool-1-thread-1开始
第二个任务执行结果:china
pool-1-thread-1结束
主线程执行结果:china
通过上述示例,我们可以得出以下结论:
1.TransmittableThreadLocal
可以让线程池中的上下文保持和父线程一致;
2.TransmittableThreadLocal
解决了线程复用导致多任务共享同一个线程上下文的问题;
使用方式
包装任务
通过上述示例,我们学到了最基本的一种使用方式:
TtlRunnable.get()
,它可以用来包装Runnable
接口的所有实例;同样的,针对
Callable
下的实例,我们可以使用TtlCallable.get()
来包装
包装线程池
为了我们在使用线程池时,不用每次都使用TtlRunnable
或TtlCallable
来包装所有任务,TransmittableThreadLocal
还提供了包装线程池的方法:
TtlExecutors.getTtlExecutor(Executors.newFixedThreadPool(1));
通过包装好的线程池,我们可以修改一下上面的示例代码:
private static TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>();
// 使用只有一个线程的线程池,测试线程复用是否影响TransmittableThreadLocal的效果
private static final Executor EXECUTOR = TtlExecutors.getTtlExecutor(Executors.newFixedThreadPool(1));
public static void main(String[] args) throws InterruptedException {
// 设置主线程的上下文为"china"
CONTEXT.set("china");
// 创建第一个任务,通过TtlRunnable.get()包装;
// 在第一个任务中查看上下文数据,检查是否拿到正确的上下文;
// 另外再修改掉该上下文,主要测试是否会影响第二个任务的上下文;
Runnable task1 = () -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "开始");
String countryCode = CONTEXT.get();
System.out.println("第一个任务执行结果:" + countryCode);
// 修改该线程中上下文值,检查是否影响第二个任务
CONTEXT.set("US");
System.out.println(thread.getName() + "结束");
};
// 第二个任务主要测试上下文是否受第一个任务的影响
Runnable task2 = () -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "开始");
String countryCode = CONTEXT.get();
System.out.println("第二个任务执行结果:" + countryCode);
System.out.println(thread.getName() + "结束");
};
// 按顺序执行两个任务,全部放到线程池中执行
CompletableFuture.runAsync(task1, EXECUTOR1)
.thenRunAsync(task2, EXECUTOR1);
// 检查主线程上下文是否受影响;
String countryCode = CONTEXT.get();
System.out.println("主线程执行结果:" + countryCode);
Thread.sleep(10000);
}
1.可以看出,我们包装好线程池后,就不再需要包装任务了,所有的任务都不需要TtlRunnable.get()
;
2.从包装好的线程池中我们可以发现,返回的实例其实是ExecutorTtlWrapper
对象,里面的submit
方法、execute()
方法上把传进去Runnable
参数使用TtlRunnable.get()
做了一层包装;
小结
本文从业务角度切入,通过层层递进的方式从ThreadLocal
、InheritableThreadLocal
在业务上的应用及产生的相关问题点,逐步引出TransmittableThreadLocal
,通过示例的方式验证TransmittableThreadLocal
符合我们的需求,并且了解了TransmittableThreadLocal
针对任务及线程池的使用方式:
1.针对任务Runnable
、Callable
实例,使用TtlRunnable.get()
、TtlCallable.get()
包装;
2.针对线程池,使用TtlExecutors.getTtlExecutor()
包装;
来源:https://juejin.cn/post/7171019628750209031


猜你喜欢
- C# using 三种使用方式介绍1.using指令。using + 命名空间名字,这样可以在程序中直接用命令空间中的类型,而不必指定类型的
- 前言最近在学习Spring Boot结合Redis时看了一些网上的教程,发现这些教程要么比较老,要么不知道从哪抄得,运行起来有问题。这里分享
- 今天突发奇想,想做一个智能拼图游戏来给哄女友。需要实现这些功能第一图片自定义第二宫格自定义,当然我一开始就想的是3*3 4*4 5*5,没有
- C#实现的对两个Table进行Merge,两表必须存在至少一个公共栏位作为连接项,否则连接就失去了意义。如下是对两个table进行Merge
- 在Android中,Activity主要负责前台页面的展示,Service主要负责需要长期运行的任务,所以在我们实际开发中,就会常常遇到Ac
- mybatis-plus框架功能很强大,把很多功能都集成了,比如自动生成代码结构,mybatis crud封装,分页,动态数据源等等,附上官
- 即只能在组件布局代码后,或者在组件的前面添加注释。#注释格式:Android的XML文件注释一般采用 <!--注释内容 -->的
- 使用函数detectAndCompute()检测关键点并计算描述符函数detectAndCompute()参数说明:void detectA
- 简介本文主要讲解java 利用Selenium 操作浏览器网站时候,需要用的js的地方,代码该如何实现。调用JavaScriptwebdri
- 最近项目上要实现语音搜索功能,界面样式要模仿一下UC浏览器的样式,UC浏览器中有一个控件,会随着声音大小浮动,然后寻思偷个懒,百
- 异步、多线程、任务、并行编程之一:选择合适的多线程模型本篇概述:@FCL4.0中已经存在的线程模型,以及它们之间异同点;@多线程编程模型的选
- 在开发程序的过程中,难免少不了写入错误日志这个关键功能。实现这个功能,可以选择使用第三方日志插件,也可以选择使用数据库,还可以自己写个简单的
- 1、多个线程对同一个队列进行读写操作,要注意进行读写控制,某个线程在读取的时候,不允许其它线程读、写;某个线程在写的时候,不允许其它线程进行
- 之前写过一篇刷新加载《RecyclerView上拉加载和下拉刷新(基础版)》 ,这次是进行改装完善。代码中注释的很详细,所以就直接上代码了。
- 上篇给大家介绍了Spring Boot启动过程完全解析(一),大家可以点击参考下该说refreshContext(context)了,首先是
- Service是Android中一个类,它是Android 四大组件之一,使用Servic
- java调用Rsync并发迁移数据并执行校验java代码如下RsyncFile.javaimport lombok.NoArgsConstr
- 本文以实例形式讲述了Android Touch事件分发过程,对于深入理解与掌握Android程序设计有很大的帮助作用。具体分析如下:首先,从
- 前言相信大家在写前端脚本的时候经常会遇到发送数据到后台的情况,但是由于浏览器的限制,不同域名之间的数据是不能互相访问的,那前端怎么和后端如何
- 本文实例讲述了C#编程读取文档Doc、Docx及Pdf内容的方法。分享给大家供大家参考。具体分析如下:Doc文档:Microsoft Wor