2. ThreadLocal 的坑

在多线程环境下使用 ThreadLocal 存储上下文时有个经典问题:跨线程访问时值会丢失。看个简单例子:

@Test
void givenThreadLocal_whenTryingToGetValueFromAnotherThread_thenNullIsExpected() {

    ThreadLocal<String> transactionID = new ThreadLocal<>();
    transactionID.set(UUID.randomUUID().toString());
    new Thread(() -> assertNull(transactionID.get())).start();
}

主线程设置的 UUID,在子线程里直接拿不到,返回 null。这个坑太基础了,老手应该都踩过。

3. InheritableThreadLocal 的局限

InheritableThreadLocal 能解决子线程继承主线程值的问题,但动态修改值时又出新问题

@Test
void givenInheritableThreadLocal_whenChangeTheTransactionIdAfterSubmissionToThreadPool_thenNewValueWillNotBeAvailableInParallelThread() {

    String firstTransactionIDValue = UUID.randomUUID().toString();
    InheritableThreadLocal<String> transactionID = new InheritableThreadLocal<>();
    transactionID.set(firstTransactionIDValue);
    Runnable task = () -> assertEquals(firstTransactionIDValue, transactionID.get());

    ExecutorService executorService = Executors.newFixedThreadPool(1);
    executorService.submit(task);

    String secondTransactionIDValue = UUID.randomUUID().toString();
    Runnable task2 = () -> assertNotEquals(secondTransactionIDValue, transactionID.get());
    transactionID.set(secondTransactionIDValue);
    executorService.submit(task2);

    executorService.shutdown();
}

关键点:

  1. ✅ 首次提交任务时能拿到初始值
  2. ❌ 主线程更新值后,线程池里的任务拿不到新值
  3. ⚠️ 原因:线程复用时上下文不会重新传递

4. 使用 transmittable-thread-local 库

阿里巴巴开源的 TransmittableThreadLocal 扩展了 InheritableThreadLocal,专门解决线程池上下文传递问题。核心优势:动态修改值也能同步到所有线程

4.1. Maven 依赖

先加依赖(当前最新版 2.14.5):

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.5</version>
</dependency>

4.2. 单线程测试

验证基础跨线程传递:

@Test
void givenTransmittableThreadLocal_whenTryingToGetValueFromAnotherThread_thenValueIsPresent() {
    TransmittableThreadLocal<String> transactionID = new TransmittableThreadLocal<>();
    transactionID.set(UUID.randomUUID().toString());
    new Thread(() -> assertNotNull(transactionID.get())).start();
}

直接继承主线程的值,和 InheritableThreadLocal 行为一致。

4.3. 线程池动态更新测试

重点来了:线程池里也能拿到动态更新的值

@Test
void givenTransmittableThreadLocal_whenChangeTheTransactionIdAfterSubmissionToThreadPool_thenNewValueWillBeAvailableInParallelThread() {

    String firstTransactionIDValue = UUID.randomUUID().toString();
    String secondTransactionIDValue = UUID.randomUUID().toString();

    TransmittableThreadLocal<String> transactionID = new TransmittableThreadLocal<>();
    transactionID.set(firstTransactionIDValue);

    Runnable task = () -> assertEquals(firstTransactionIDValue, transactionID.get());
    Runnable task2 = () -> assertEquals(secondTransactionIDValue, transactionID.get());

    ExecutorService executorService = Executors.newFixedThreadPool(1);
    executorService.submit(TtlRunnable.get(task));  // 关键包装

    transactionID.set(secondTransactionIDValue);
    executorService.submit(TtlRunnable.get(task2)); // 关键包装

    executorService.shutdown();
}

核心技巧:

  • ✅ 用 TtlRunnable 包装任务
  • ✅ 确保每次提交时都传递最新上下文
  • ✅ 即使线程复用也能拿到新值

4.4. 并行流场景

并行流底层用 ForkJoinPool,上下文传递更复杂。解决方案:用 TtlExecutors 包装线程池

@Test
void givenTransmittableThreadLocal_whenChangeTheTransactionIdAfterParallelStreamAlreadyProcessed_thenNewValueWillBeAvailableInTheSecondParallelStream() {

    String firstTransactionIDValue = UUID.randomUUID().toString();
    String secondTransactionIDValue = UUID.randomUUID().toString();

    TransmittableThreadLocal<String> transactionID = new TransmittableThreadLocal<>();
    transactionID.set(firstTransactionIDValue);

    TtlExecutors.getTtlExecutorService(new ForkJoinPool(4))  // 包装线程池
      .submit(
          () -> List.of(1, 2, 3, 4, 5)
            .parallelStream()
            .forEach(i -> assertEquals(firstTransactionIDValue, transactionID.get())));

    transactionID.set(secondTransactionIDValue);
    TtlExecutors.getTtlExecutorService(new ForkJoinPool(4))  // 再次包装
      .submit(
          () -> List.of(1, 2, 3, 4, 5)
            .parallelStream()
            .forEach(i -> assertEquals(secondTransactionIDValue, transactionID.get())));
}

关键点:

  • ⚠️ 并行流默认使用公共 ForkJoinPool,无法直接修改
  • ✅ 通过自定义 ForkJoinPool + TtlExecutors 包装解决
  • ✅ 确保每个并行流任务都能拿到最新上下文

5. 总结

根据场景选对工具:

场景 推荐方案 特点
单线程上下文 ThreadLocal 简单高效
父子线程传递 InheritableThreadLocal 继承初始值
线程池动态更新 TransmittableThreadLocal 实时同步上下文

踩坑指南:

  1. ❌ 普通线程池 + ThreadLocal → 上下文丢失
  2. ❌ 线程池 + InheritableThreadLocal → 动态更新失效
  3. ✅ 线程池 + TransmittableThreadLocal + 包装器 → 全场景通吃

代码已上传到 GitHub,有需要自取。


原始标题:Introduction to transmittable-thread-local | Baeldung