还在为线程间上下文传递而烦恼,用TransmittableThreadLocal试试

简介: 还在为线程间上下文传递而烦恼,用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, EXECUTOR)
        .thenRunAsync(task2, EXECUTOR);
    // 检查主线程上下文是否受影响;
    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()来包装
  • 包装线程池
    为了我们在使用线程池时,不用每次都使用TtlRunnableTtlCallable来包装所有任务,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, EXECUTOR)
        .thenRunAsync(task2, EXECUTOR);
    // 检查主线程上下文是否受影响;
    String countryCode = CONTEXT.get();
    System.out.println("主线程执行结果:" + countryCode);
    Thread.sleep(10000);
  }
复制代码

1.可以看出,我们包装好线程池后,就不再需要包装任务了,所有的任务都不需要TtlRunnable.get()

2.从包装好的线程池中我们可以发现,返回的实例其实是ExecutorTtlWrapper对象,里面的submit方法、execute()方法上把传进去Runnable参数使用TtlRunnable.get()做了一层包装;

小结

本文从业务角度切入,通过层层递进的方式从ThreadLocalInheritableThreadLocal在业务上的应用及产生的相关问题点,逐步引出TransmittableThreadLocal,通过示例的方式验证TransmittableThreadLocal符合我们的需求,并且了解了TransmittableThreadLocal针对任务及线程池的使用方式:

1.针对任务RunnableCallable实例,使用TtlRunnable.get()TtlCallable.get()包装;

2.针对线程池,使用TtlExecutors.getTtlExecutor()包装;



相关文章
解决开启子线程,导致request上下文和session信息丢失问题
解决开启子线程,导致request上下文和session信息丢失问题
1143 0
|
7月前
|
设计模式 监控 安全
多线程设计模式【多线程上下文设计模式、Guarded Suspension 设计模式、 Latch 设计模式】(二)-全面详解(学习总结---从入门到深化)
多线程设计模式【多线程上下文设计模式、Guarded Suspension 设计模式、 Latch 设计模式】(二)-全面详解(学习总结---从入门到深化)
121 0
|
2月前
|
安全 调度 C#
STA模型、同步上下文和多线程、异步调度
【10月更文挑战第19天】本文介绍了 STA 模型、同步上下文和多线程、异步调度的概念及其优缺点。STA 模型适用于单线程环境,确保资源访问的顺序性;同步上下文和多线程提高了程序的并发性和响应性,但增加了复杂性;异步调度提升了程序的响应性和资源利用率,但也带来了编程复杂性和错误处理的挑战。选择合适的模型需根据具体应用场景和需求进行权衡。
|
5月前
|
存储 前端开发 Java
(二)JVM成神路之剖析Java类加载子系统、双亲委派机制及线程上下文类加载器
上篇《初识Java虚拟机》文章中曾提及到:我们所编写的Java代码经过编译之后,会生成对应的class字节码文件,而在程序启动时会通过类加载子系统将这些字节码文件先装载进内存,然后再交由执行引擎执行。本文中则会对Java虚拟机的类加载机制以及执行引擎进行全面分析。
103 0
|
7月前
|
监控 Java 关系型数据库
JVM工作原理与实战(十三):打破双亲委派机制-线程上下文类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容。
410 2
|
Java 数据库连接 数据库
【面试题精讲】JVM-打破双亲委派机制-线程上下文类加载器
【面试题精讲】JVM-打破双亲委派机制-线程上下文类加载器
|
存储 Java 应用服务中间件
|
存储 Unix Linux
【操作系统】进程上下文和线程上下文
【操作系统】进程上下文和线程上下文
141 0
|
消息中间件 数据采集 Java
Flask嵌套启动子线程如何读取请求上下文?
Flask嵌套启动子线程如何读取请求上下文?
240 0
|
前端开发 Java 关系型数据库
【Java实战系列】认识一下线程上下文类加载器实现【逆向加载机制】|周末学习
【Java实战系列】认识一下线程上下文类加载器实现【逆向加载机制】|周末学习
283 0