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();
}
关键点:
- ✅ 首次提交任务时能拿到初始值
- ❌ 主线程更新值后,线程池里的任务拿不到新值
- ⚠️ 原因:线程复用时上下文不会重新传递
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 |
实时同步上下文 |
踩坑指南:
- ❌ 普通线程池 +
ThreadLocal
→ 上下文丢失 - ❌ 线程池 +
InheritableThreadLocal
→ 动态更新失效 - ✅ 线程池 +
TransmittableThreadLocal
+ 包装器 → 全场景通吃
代码已上传到 GitHub,有需要自取。