TransmittableThreadLocal解决线程间上下文传递烦恼
前言
在一些项目中,经常会遇到需要把当前线程中的上下文传递到其他线程中的情况,比如某项目包含国际化操作,在业务请求进来时需要把对应的国家代码存储到当前线程中,以便后续的业务逻辑能够根据国家代码正确地处理;另外在一些异步化操作中,也要保证异常线程中也能够正确地获取到对应的国家代码。
在上述业务场景中,我们很自然的就想到了使用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()
包装;
以上就是TransmittableThreadLocal解决线程间上下文传递烦恼的详细内容,更多关于TransmittableThreadLocal线程传递的资料请关注脚本之家其它相关文章!
相关文章
SpringBoot2.x 整合 thumbnailator 图片处理的示例代码
这篇文章主要介绍了SpringBoot2.x 之整合 thumbnailator 图片处理,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2020-10-10SpringBoot Starter机制及整合tomcat的实现详解
这篇文章主要介绍了SpringBoot Starter机制及整合tomcat的实现,我们知道SpringBoot自己在“后台”帮我们配置了很多原本需要我们手动去的东西,至于这个“后台”是啥,就是Starter机制2022-09-09java 使用idea将工程打成jar并创建成exe文件类型执行的方法详解
这篇文章主要介绍了java 使用idea将工程打成jar并创建成exe文件类型执行,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧2020-09-09Java Mybatis的初始化之Mapper.xml映射文件的详解
这篇文章主要介绍了Java Mybatis的初始化之Mapper.xml映射文件的详解,解析完全局配置文件后接下来就是解析Mapper文件了,它是通过XMLMapperBuilder来进行解析的2022-08-08Spring Data Jpa实现自定义repository转DTO
这篇文章主要介绍了Spring Data Jpa实现自定义repository转DTO,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下2020-08-08SpringBoot+MyBatisPlus+Vue 前后端分离项目快速搭建过程(后端)
这篇文章主要介绍了SpringBoot+MyBatisPlus+Vue 前后端分离项目快速搭建过程(后端),快速生成后端代码、封装结果集、增删改查、模糊查找,毕设基础框架,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2021-05-05
最新评论