解决父子线程参数传递 TransmittableThreadLocal

TransmittableThreadLocal 详解与用法

TransmittableThreadLocal(简称 TTL)是由阿里巴巴开源的线程上下文传递工具,用于解决多线程、线程池、异步任务中 ThreadLocal 上下文丢失或污染的问题。


📌 为什么需要它?

❗传统 ThreadLocal 的局限性

  • 每个线程有自己的副本。
  • 在线程池场景下,线程会被复用,导致:
    • 上下文污染:前一个任务设置的数据被后一个任务读取。
    • 上下文丢失:异步任务无法继承主线程的上下文。

✅ TransmittableThreadLocal 的优势

  • 支持线程池、CompletableFuture、@Async 等异步/并发场景。
  • 自动处理上下文的传递和清理。
  • 提供线程安全的上下文隔离机制。

🔧 使用步骤详解

1. 引入依赖(Maven)

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

Gradle 用户可使用:

implementation 'com.alibaba:transmittable-thread-local:2.12.0'

2. 替换 ThreadLocalTransmittableThreadLocal

修改你的 TokenUserUtil.java

原代码:

private static ThreadLocal<TokenUser> tokenUserThreadLocal = new ThreadLocal<>();

修改为:

import com.alibaba.ttl.TransmittableThreadLocal;


public class TokenUserUtil {


    // 使用 TransmittableThreadLocal 替代 ThreadLocal
    private static final ThreadLocal<TokenUser> tokenUserThreadLocal = new TransmittableThreadLocal<>();


    public static void setTokenUser(TokenUser tokenUser) {
        tokenUserThreadLocal.set(tokenUser);
    }


    public static TokenUser getTokenUser() {
        TokenUser tokenUser = tokenUserThreadLocal.get();
        if (tokenUser == null) {
            tokenUser = new TokenUser();
        }
        return tokenUser;
    }
}

3. 包装线程池(推荐)

如果你自己定义了线程池(如 parallelExecutor),需要将其包装成支持 TTL 的版本。

示例配置:

@Bean("parallelExecutor")
public ExecutorService parallelExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(10);
    executor.setQueueCapacity(200);
    executor.setThreadNamePrefix("import-pool-");
    executor.initialize();


    // 【关键步骤】使用 TtlExecutors 包装线程池,确保上下文传递
    return TtlExecutors.getTtlExecutorService(executor);
}

这样,你就可以在异步任务中正确继承主线程的上下文。

真实系统代码

@Configuration
public class ThreadPoolConfig {

  /**
 * 核心线程数
  * 默认的核心线程数为1
 */  private static final int CORE_POOL_SIZE = 5;
  /**
 * 最大线程数
  * 默认的最大线程数是Integer.MAX_VALUE 即2<sup>31</sup>-1
 */  private static final int MAX_POOL_SIZE = 50;
  /**
 * 缓冲队列数
  * 默认的缓冲队列数是Integer.MAX_VALUE 即2<sup>31</sup>-1
 */  private static final int QUEUE_CAPACITY = 100;

  /**
 * 允许线程空闲时间
  * 默认的线程空闲时间为60秒
  */
  private static final int KEEP_ALIVE_SECONDS = 30;

  /**
 * 线程池前缀名
  */
  private static final String THREAD_NAME_PREFIX = "YiLi_Service_Async_";

  /**
 * allowCoreThreadTimeOut为true则线程池数量最后销毁到0个
  * allowCoreThreadTimeOut为false
 * 销毁机制:超过核心线程数时,而且(超过最大值或者timeout过),就会销毁。
  * 默认是false
 */  private boolean allowCoreThreadTimeOut = false;

  @Bean("parallelExecutor")
  public ThreadPoolTaskExecutor taskExecutor() {

  ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
  taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
  taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
  taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
  taskExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
  taskExecutor.setThreadNamePrefix(THREAD_NAME_PREFIX);
  taskExecutor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut);
  taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
  //线程池初始化
  taskExecutor.initialize();
  return taskExecutor;
  }
}

4. 使用方式(无需额外操作)

你现在可以像以前一样使用 TokenUserUtil.setTokenUser(...)TokenUserUtil.getTokenUser(),它会自动处理:

  • 主线程 → 子线程上下文传递;
  • 多个异步任务之间互不干扰;
  • 线程复用时自动清理上下文。

例如:

CompletableFuture.runAsync(() -> {
    // 这里可以直接获取到主线程设置的 TokenUser
    TokenUser user = TokenUserUtil.getTokenUser();
    log.info("当前用户:{}", user.getUsername());
}, parallelExecutor);

🧪 示例说明

假设你有如下逻辑:

TokenUserUtil.setTokenUser(userA);
CompletableFuture.runAsync(() -> {
    System.out.println(TokenUserUtil.getTokenUser().getUsername()); // 应输出 userA
}, parallelExecutor);


TokenUserUtil.setTokenUser(userB);
CompletableFuture.runAsync(() -> {
    System.out.println(TokenUserUtil.getTokenUser().getUsername()); // 应输出 userB
}, parallelExecutor);

即使两个任务被同一个线程执行,它们也会分别读取到正确的上下文。


🛠️ 高级功能(可选)

1. 手动控制上下文快照

你可以手动捕获当前线程的上下文,并在其他线程中恢复它:

// 捕获上下文
TransmittableThreadLocal<Map<String, Object>> contextSnapshot = TransmittableThreadLocal.copy();


// 在另一个线程中恢复
contextSnapshot.restore();

适用于复杂嵌套调用或自定义调度器。


⚠️ 注意事项

项目 说明
不要混用不同上下文工具 比如同时使用 InheritableThreadLocalTransmittableThreadLocal 可能会导致不可预料的行为。
Spring @Async 默认线程池也需装饰 如果你也用了 @Async,请确认其使用的线程池已通过 TtlExecutors 包装。
日志链路建议配合 MDC 使用 TTL 如果你也使用 MDC 做日志链路追踪,请使用 TtlMDCAdapter 替代默认实现。

✅ 总结

功能 描述
替换 ThreadLocal 使用 TransmittableThreadLocal 替代原有类
包装线程池 使用 TtlExecutors.getTtlExecutorService(...) 装饰线程池
直接使用 不用关心上下文传递,自动继承主线程数据
安全性 线程复用不会造成上下文污染
兼容性 支持 CompletableFuture、@Async、线程池等所有异步场景

📚 推荐资料


Comments