我们来全面深入地探讨 TransmittableThreadLocal
(TTL)。这是一个在异步编程中极其重要的工具,特别是在使用线程池的场景下。
1. 它是什么?
TransmittableThreadLocal
是阿里巴巴开源的库,是 InheritableThreadLocal
的增强版。它解决了 InheritableThreadLocal
在线程池等复用线程的场景下无法正确传递线程本地变量的问题。
2. 核心使用场景
traceId
来串联所有日志。TTL 可以确保这个 traceId
在每次异步调用时都能被正确传递。ThreadLocal
中。当业务逻辑切换到线程池中执行异步任务时,TTL 可以自动将这些信息传递过去,避免在代码中显式地传递参数。3. 与标准库类的对比
特性 | ThreadLocal |
InheritableThreadLocal |
TransmittableThreadLocal (TTL) |
---|---|---|---|
基本功能 | 在当前线程存储数据 | 继承自 ThreadLocal ,创建新线程时能将数据从父线程拷贝到子线程。 |
继承自 InheritableThreadLocal ,具备其所有能力。 |
线程池场景 | 完全失效。线程被复用,任务执行时获取到的是之前任务设置的值或 null 。 |
完全失效。线程池中的线程是已创建好的,不会再次触发拷贝。 | 完美解决。通过修饰 Runnable /Callable (或使用 Java Agent),在任务提交时捕捉上下文,在任务执行时恢复上下文。 |
适用场景 | 简单的同步编程,线程内部上下文管理。 | 简单的父子线程单向传递,且子线程不会被复用。 | 复杂的异步编程,尤其是使用线程池、CompletableFuture 、并行流等场景。 |
首先需要引入 Maven 依赖:
com.alibaba
transmittable-thread-local
2.14.5
场景模拟:我们有一个 Web 拦截器,在请求开始时将 traceId
放入上下文。随后,业务逻辑将任务提交到线程池进行异步处理,我们希望异步任务能打印出正确的 traceId
。
TtlRunnable
/TtlCallable
装饰(手动方式)这是最常用和推荐的方式。
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TtlExample {
// 1. 使用 TransmittableThreadLocal 定义上下文
private static final TransmittableThreadLocal context = new TransmittableThreadLocal();
// 2. 创建一个固定线程池(模拟业务中共享的线程池)
private static final ExecutorService executorService = Executors.newFixedThreadPool(2);
public static void main(String[] args) throws InterruptedException {
// 模拟Web过滤器:在主线程设置 traceId
context.set("traceId-12345");
// 3. 创建原始任务
Runnable task = () -> {
// 在线程池的线程中执行时,这里能获取到之前设置的 traceId
String traceId = context.get();
System.out.println("Async thread: " + Thread.currentThread().getName() + ", traceId: " + traceId);
};
// 4. 【关键】使用 TtlRunnable 装饰原始任务
Runnable ttlTask = TtlRunnable.get(task);
// 5. 提交被装饰后的任务
executorService.submit(ttlTask);
// 主线程清空上下文,不影响已捕获的上下文
context.remove();
// 再提交一个任务,验证线程池线程复用后的情况
Thread.sleep(100); // 等待一下确保第一个任务执行完
context.set("traceId-67890");
executorService.submit(TtlRunnable.get(() -> {
System.out.println("Async thread: " + Thread.currentThread().getName() + ", traceId: " + context.get());
}));
executorService.shutdown();
}
}
输出结果:
Async thread: pool-1-thread-1, traceId: traceId-12345
Async thread: pool-1-thread-1, traceId: traceId-67890
可以看到,尽管是同一个线程 pool-1-thread-1
执行了两个任务,但每个任务都拿到了提交时正确的 traceId
,完美解决了线程复用带来的串号问题。
TtlExecutors
装饰线程池(更优雅的方式)这种方式可以一劳永逸,对代码侵入性最小。
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TtlExecutorServiceExample {
private static final TransmittableThreadLocal context = new TransmittableThreadLocal();
public static void main(String[] args) {
// 1. 创建原始线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 2. 【关键】使用 TtlExecutors 装饰线程池
ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(executorService);
// 模拟设置上下文
context.set("traceId-abcde");
// 3. 直接向被装饰的线程池提交任务,无需再手动装饰 Runnable
ttlExecutorService.submit(() -> {
System.out.println("Async thread: " + Thread.currentThread().getName() + ", traceId: " + context.get());
});
ttlExecutorService.shutdown();
}
}
内存泄漏
ThreadLocal
一样,TTL 变量是线程的强引用,而线程池中的线程是长期存活(强引用)的。如果不再需要的上下文数据没有及时调用 remove()
方法清理,它会一直存在于线程的 ThreadLocalMap
中,导致内存泄漏。finally
代码块中清理上下文。TTL 的最佳实践是,在任务执行完毕后,自动恢复并清理。Runnable ttlTask = TtlRunnable.get(() -> {
try {
// ... 业务逻辑
String value = context.get(); // 获取到的是提交时的值
// ... 更多业务逻辑
} finally {
// TTL 会自动在任务执行前后做快照和恢复,
// 这里清理的是当前任务线程的上下文,不会影响提交线程的原始上下文。
context.remove();
}
});
空值(Null Value)
get()
为 null
),那么异步任务线程中获取到的也是 null
。性能开销
与 InheritableThreadLocal 的兼容性
InheritableThreadLocal
,所以一个 TransmittableThreadLocal
变量同样具备在创建新线程时传递值的能力。使用 TtlExecutors
装饰线程池
TtlRunnable
。定义上下文包装类
Context
类,并使用一个单例的 TTL 来持有这个上下文对象。public class RequestContext {
private String traceId;
private String userId;
private String locale;
// ... getters and setters
}
public class ContextHolder {
private static final TransmittableThreadLocal context = new TransmittableThreadLocal();
public static void set(RequestContext requestContext) {
context.set(requestContext);
}
public static RequestContext get() {
return context.get();
}
public static void remove() {
context.remove();
}
}
与 Spring 等框架集成
Filter
或 Interceptor
中初始化上下文(如解析鉴权信息生成 TraceId
)。@Async
异步任务中,如果需要传递上下文,你可以:
AsyncConfigurer
,返回一个被 TtlExecutors
装饰过的 TaskExecutor
。TtlRunnable
包装(不太优雅)。清晰的生命周期管理
finally
块)清理主线程的上下文;在每个异步任务的 finally
块中清理当前任务线程的上下文。谨慎使用
ThreadLocal
即可。不必要的使用会增加复杂性和性能开销。总之,TransmittableThreadLocal
是处理 Java 异步编程中上下文传递问题的“银弹”,正确理解和使用它能极大地提升分布式系统和复杂异步流程的可维护性和可观测性。
参与评论
手机查看
返回顶部