解决父子线程参数传递 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. 替换 ThreadLocal 为 TransmittableThreadLocal
修改你的 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();
适用于复杂嵌套调用或自定义调度器。
⚠️ 注意事项
| 项目 | 说明 |
|---|---|
| 不要混用不同上下文工具 | 比如同时使用 InheritableThreadLocal 和 TransmittableThreadLocal 可能会导致不可预料的行为。 |
| Spring @Async 默认线程池也需装饰 | 如果你也用了 @Async,请确认其使用的线程池已通过 TtlExecutors 包装。 |
| 日志链路建议配合 MDC 使用 TTL | 如果你也使用 MDC 做日志链路追踪,请使用 TtlMDCAdapter 替代默认实现。 |
✅ 总结
| 功能 | 描述 |
|---|---|
替换 ThreadLocal |
使用 TransmittableThreadLocal 替代原有类 |
| 包装线程池 | 使用 TtlExecutors.getTtlExecutorService(...) 装饰线程池 |
| 直接使用 | 不用关心上下文传递,自动继承主线程数据 |
| 安全性 | 线程复用不会造成上下文污染 |
| 兼容性 | 支持 CompletableFuture、@Async、线程池等所有异步场景 |
📚 推荐资料
- GitHub 官方文档:https://github.com/alibaba/transmittable-thread-local
- 中文 Wiki:https://github.com/alibaba/transmittable-thread-local/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97
Comments