一、原生 ThreadLocal 的痛点
Java 原生 ThreadLocal 作用:在当前线程内共享变量,线程间隔离 (比如存储用户登录信息、链路追踪 ID、请求上下文)。 但它有一个致命问题: 在使用线程池(ThreadPoolExecutor)、异步任务(@Async、CompletableFuture)时,子线程 / 线程池中的线程,完全拿不到父线程的 ThreadLocal 变量!
举个场景:
- 主线程(请求线程)存入
ThreadLocal:用户 ID、traceId(链路 ID) - 交给线程池执行异步任务
- 异步线程里,
ThreadLocal.get()直接返回null→ 上下文丢失,导致报错、日志无 traceId、权限校验失败
二、TTL 的核心作用
TransmittableThreadLocal(TTL)= 支持在 线程池 / 异步线程 / 子线程 中,自动传递父线程的本地变量。
简单说: ✅ 原生 ThreadLocal:父子线程、线程池之间传不过去 ✅ TTL:跨线程、跨线程池,上下文变量稳稳传递
它解决的是分布式、微服务、异步化编程中最常见的问题:
- 日志链路追踪(traceId)丢失
- 用户登录上下文在异步任务中获取不到
- 多线程环境下请求上下文、自定义参数传递失效
- 线程池复用线程时,上下文污染 / 错乱
三、核心功能亮点
- 兼容线程池 不用改业务代码,只需简单修饰线程池,就能自动传递 TTL 变量。
- 无侵入性 用法和原生 ThreadLocal 几乎一样,学习成本极低。
- 解决线程复用污染 原生 ThreadLocal 在线程池里会因为线程复用,保留上一个任务的变量,导致错乱;TTL 可以自动隔离、清理。
- 支持标准异步组件 完美支持:
ThreadPoolExecutor、@Async、CompletableFuture、Dubbo、MQ 等异步场景。
四、依赖集成
xml
<!-- ttl -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.12.0</version>
</dependency>
五、使用示例
5.1. 原生 ThreadLocal vs TTL
java
/**
* TTL 完整使用示例
* 对比:原生 ThreadLocal vs TTL
*/
public class TtlFullDemo {
// 1. 原生 ThreadLocal → 异步会丢失
private static final ThreadLocal<String> NATIVE_THREAD_LOCAL = new ThreadLocal<>();
// 2. TTL → 异步可以传递
private static final TransmittableThreadLocal<String> TTL = new TransmittableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 构造一个普通线程池(模拟项目里的异步线程池)
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// ==========================================
// 重点:给线程池套上 TTL 装饰器(必须加!)
// ==========================================
ExecutorService ttlThreadPool = TtlExecutors.getTtlExecutorService(threadPool);
// ============== 1. 主线程设置值 ==============
System.out.println("===== 主线程设置上下文值 =====");
NATIVE_THREAD_LOCAL.set("我是原生ThreadLocal值");
TTL.set("我是TTL值");
System.out.println("主线程获取原生TL:" + NATIVE_THREAD_LOCAL.get());
System.out.println("主线程获取TTL:" + TTL.get());
// ============== 2. 提交异步任务 ==============
System.out.println("\n===== 异步线程开始执行 =====");
ttlThreadPool.submit(() -> {
// 异步任务里尝试获取
System.out.println("异步线程获取原生TL:" + NATIVE_THREAD_LOCAL.get()); // null(丢失)
System.out.println("异步线程获取TTL:" + TTL.get()); // 正常获取
});
// 等待异步执行完
TimeUnit.SECONDS.sleep(1);
// ============== 3. 清理资源 ==============
NATIVE_THREAD_LOCAL.remove();
TTL.remove();
threadPool.shutdown();
}
}
- 运行结果
plaintext
===== 主线程设置上下文值 =====
主线程获取原生TL:我是原生ThreadLocal值
主线程获取TTL:我是TTL值
===== 异步线程开始执行 =====
异步线程获取原生TL:null <-- 原生 ThreadLocal 丢失了!
异步线程获取TTL:我是TTL值 <-- TTL 正常传递!
5.2. TTL配合TtlExecutors 装饰
java
public class TtlBugDemo {
private static final TransmittableThreadLocal<String> TTL = new TransmittableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 核心:不使用 TtlExecutors 包装!
ExecutorService threadPool = Executors.newFixedThreadPool(1); // 单线程池,一定会复用线程
// 第一次任务:设置值为 "任务1"
TTL.set("任务1的上下文");
System.out.println("=== 提交任务1 ===");
threadPool.submit(() -> {
System.out.println("任务1获取TTL:" + TTL.get()); // 能拿到
});
TimeUnit.SECONDS.sleep(1);
// 第二次任务:主线程设置新值 "任务2"
// 重点:线程池复用了同一个线程
TTL.set("任务2的上下文");
System.out.println("\n=== 提交任务2 ===");
threadPool.submit(() -> {
System.out.println("任务2获取TTL:" + TTL.get()); // 这里会出 BUG!
});
TimeUnit.SECONDS.sleep(1);
threadPool.shutdown();
}
}
- 错误结果
plaintext
=== 提交任务1 ===
任务1获取TTL:任务1的上下文
=== 提交任务2 ===
任务2获取TTL:任务1的上下文 ← 【严重BUG】拿到了旧的上下文!
- 加上TtlExecutors修饰后
java
// 只加这一行
ExecutorService ttlThreadPool = TtlExecutors.getTtlExecutorService(threadPool);
// 提交任务必须用 ttlThreadPool
ttlThreadPool.submit(...)
- 正确结果
plaintext
=== 提交任务1 ===
任务1获取TTL:任务1的上下文
=== 提交任务2 ===
任务2获取TTL:任务2的上下文 ← 正常!
六、核心总结
在使用 TransmittableThreadLocal(TTL)实现异步线程上下文传递时,**TtlExecutors.getTtlExecutorService()**必须使用 装饰线程池。
如果不装饰,在线程池线程复用 场景下,会出现上下文获取旧值、数据错乱的严重 BUG,导致线上用户信息、链路追踪、权限校验等业务异常。