项目从JDK8升级到JDK17之后,CompletableFuture出现了致命BUG!
前言
最近缺项目经历想快速提升项目实战能力(包含多个AI项目),或者最近找工作,或者想学习AI的小伙伴,可以看看下面👇🏻的这个链接(或许真的能够帮到你)。
最近有位球友找到我,说他们项目从JDK8升级到JDK17后,一个CompletableFuture异步任务里死活拿不到RequestContextHolder中的用户信息了。
以前在JDK8里跑得好好的,一升到17,RequestContextHolder.getRequestAttributes()永远返回null。
这不是个例。
今天就跟大家专门聊聊这个话题,希望对你会有所帮助。
一、“翻车”现场
我们先来看一段“人畜无害”的代码:
@RestController
public class TestController {
@GetMapping("/async")
public CompletableFuture<String> test() {
// 主线程中能拿到RequestAttributes
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
System.out.println("主线程: " + attrs);
return CompletableFuture.supplyAsync(() -> {
// 子线程中拿RequestAttributes
RequestAttributes subAttrs = RequestContextHolder.getRequestAttributes();
System.out.println("子线程: " + subAttrs);
return subAttrs != null ? "success" : "fail";
});
}
}在JDK8下运行,输出:
主线程: org.springframework.web.context.request.ServletRequestAttributes@...
子线程: org.springframework.web.context.request.ServletRequestAttributes@...一切正常,子线程成功拿到了请求上下文。
在JDK17下运行,输出:
主线程: org.springframework.web.context.request.ServletRequestAttributes@...
子线程: null子线程拿不到了!
这就是升级后第一个“惊喜”。
二、JDK8能拿到,JDK17却不行?
2.1 RequestContextHolder的原理
Spring的RequestContextHolder底层用的是ThreadLocal。
为了支持子线程继承,Spring还提供了InheritableThreadLocal模式。
在Spring Boot中,默认配置下,RequestContextHolder使用的是NamedInheritableThreadLocal(继承自InheritableThreadLocal)。
在JDK8中,InheritableThreadLocal的变量会自动传递给新创建的子线程。
但注意,这里有一个关键前提:子线程必须是由父线程直接创建的。
CompletableFuture默认使用ForkJoinPool.commonPool()来执行异步任务。
这个线程池的worker线程并不是在调用时由父线程创建的,而是由ForkJoinPool预先创建或动态创建的。
那么,InheritableThreadLocal的自动传递机制是否还适用?
2.2 ForkJoinPool的“继承”行为不一样!
这是最核心的原因。
在JDK8中,ForkJoinPool的ForkJoinWorkerThread在初始化时,会尝试从创建它的父线程中拷贝InheritableThreadLocal的值。
虽然这个行为依赖于具体实现,但实测确实会继承。
因此,你在主线程里设置的RequestAttributes,能被CompletableFuture的子任务拿到。
在JDK9+(包括JDK17)中,ForkJoinPool.commonPool()的实现发生了重要变化:
- commonPool的worker线程是全局共享的,且它的创建时机可能比你的业务线程要早。
- 更重要的是,ForkJoinWorkerThread的构造函数不再主动拷贝父线程的InheritableThreadLocal。这意味着即使父线程有值,子线程也拿不到。
这就是“升级就崩”的根本原因。
2.3 源码对比,看到底变了什么
来看一下JDK8中ForkJoinWorkerThread的构造函数(简化版):
// JDK8
ForkJoinWorkerThread(ForkJoinPool pool, ThreadGroup threadGroup) {
super(threadGroup, null);
this.pool = pool;
// 关键:拷贝父线程的InheritableThreadLocal
this.inheritableThreadLocals = Thread.currentThread().inheritableThreadLocals;
}而在JDK17中,这个构造函数已经不拷贝了。
// JDK 17: ForkJoinWorkerThread.java (src/java.base/share/classes/java/util/concurrent/)
ForkJoinWorkerThread(ForkJoinPool pool, ThreadGroup threadGroup) {
super(threadGroup, null);
this.pool = pool;
// 没有 this.inheritableThreadLocals = ... 代码
}最近缺项目经历想快速提升项目实战能力(包含多个AI项目),或者最近找工作,或者想学习AI的小伙伴,可以看看下面👇🏻的这个链接(或许真的能够帮到你)。
实际上,ForkJoinWorkerThread的创建更加底层,不再直接处理InheritableThreadLocal的传递。
取而代之的是,如果你需要传递,必须显式通过其他方式。
三、还有哪些场景会踩坑?
除了RequestContextHolder,只要是使用InheritableThreadLocal传递上下文的场景,在CompletableFuture中都会遇到同样的问题。比如:
- 自定义的
InheritableThreadLocal存用户ID、TraceId - Spring Security的
SecurityContextHolder(默认使用ThreadLocal,不会继承,但配置了MODE_INHERITABLETHREADLOCAL后也会失效) - MDC(日志链路追踪):比如
org.slf4j.MDC,默认也是InheritableThreadLocal,升级后异步日志会丢traceId。
四、解决方案(附代码)
方案一:手动传递(最直接,无侵入)
@GetMapping("/async")
public CompletableFuture<String> test() {
// 在主线程中先获取RequestAttributes
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
return CompletableFuture.supplyAsync(() -> {
// 手动设置到子线程
RequestContextHolder.setRequestAttributes(attrs);
try {
// 业务代码
return "success";
} finally {
// 清理,防止内存泄漏
RequestContextHolder.resetRequestAttributes();
}
});
}优点:简单有效,不依赖额外类库。缺点:每个异步调用都要写一堆模板代码。
方案二:自定义线程池 + 包装任务
// 创建自定义线程池,并重写execute方法,传递InheritableThreadLocal
public class ContextAwareThreadPool extends ThreadPoolExecutor {
public ContextAwareThreadPool(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
// 捕获父线程的InheritableThreadLocal值(这里以RequestAttributes为例)
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
super.execute(() -> {
RequestContextHolder.setRequestAttributes(attrs);
try {
command.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
});
}
}
// 使用时指定线程池
ExecutorService executor = new ContextAwareThreadPool(5, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
CompletableFuture.supplyAsync(() -> {
// 这里能拿到RequestAttributes
return "ok";
}, executor);优点:一次配置,全局生效。缺点:需要自定义线程池,且CompletableFuture需要显式传入。
方案三:使用TransmittableThreadLocal(推荐)
阿里巴巴开源的TransmittableThreadLocal (TTL)专门解决线程池上下文传递问题。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.5</version>
</dependency>// 将原来的ThreadLocal或InheritableThreadLocal换成TransmittableThreadLocal
private static final TransmittableThreadLocal<RequestAttributes> ttl = new TransmittableThreadLocal<>();
// 主线程设置
ttl.set(RequestContextHolder.getRequestAttributes());
// 包装Runnable或Callable
Runnable task = () -> {
RequestAttributes attrs = ttl.get();
// ...
};
// 使用TtlRunnable包装
Runnable wrapped = TtlRunnable.get(task);
CompletableFuture.runAsync(wrapped, customExecutor);注意:TTL对于ForkJoinPool的传递需要特殊处理?实际上,对于CompletableFuture的默认线程池,TTL需要配合TtlForkJoinPool或者自己包装。更简单的做法是:指定自定义线程池,再使用TtlExecutors包装线程池。
ExecutorService executor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));
CompletableFuture.supplyAsync(() -> "ok", executor);使用TTL后,无论是普通线程池还是ForkJoinPool,都能自动传递上下文。
方案四:升级到Spring Boot 3.x + 最新Spring框架
Spring Boot 3.x对异步上下文传递做了优化。在Spring Boot 3.2+中,@Async注解默认会传递RequestAttributes,但它内部使用的是TaskDecorator。对于CompletableFuture,也可以使用TaskDecorator包装。
不过这个方案只适用于Spring管理的线程池,对CompletableFuture的默认池无效。
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setTaskDecorator(new ContextCopyingDecorator()); // 自定义装饰器
executor.initialize();
return executor;
}五、解决方案对比
| 方案 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|
| 手动传递 | 简单直接,无依赖 | 侵入性强,代码冗余 | ⭐⭐ |
| 自定义线程池包装 | 一次性配置,侵入小 | 需要改造线程池 | ⭐⭐⭐ |
| TransmittableThreadLocal | 自动传递,支持各类线程池 | 引入第三方库 | ⭐⭐⭐⭐⭐ |
| Spring Boot 3.x装饰器 | 生态标准 | 依赖Spring版本 | ⭐⭐⭐ |
综合推荐:使用TransmittableThreadLocal,它是最通用、侵入性最小的解决方案。
六、优点、缺点与使用场景
6.1 TransmittableThreadLocal的优点
- 自动传递:无需手动set,代码无侵入。
- 支持几乎所有线程池:包括ForkJoinPool、ThreadPoolExecutor等。
- 阿里生产环境验证:稳定性高。
6.2 缺点
- 引入外部依赖,需要团队学习。
- 对性能有微乎其微的影响(可忽略)。
6.3 使用场景
- 使用CompletableFuture或线程池的异步任务。
- 需要传递
RequestContextHolder、SecurityContextHolder、MDC等上下文信息。 - 从JDK8升级到高版本的项目。
七、总结:升级后,不要迷信“InheritableThreadLocal”
JDK8 → JDK17,最大的坑之一就是ForkJoinPool不再自动传递InheritableThreadLocal。这导致大量使用CompletableFuture的代码出现上下文丢失。究其根本,是因为高版本JDK对ForkJoinPool的worker线程创建机制进行了优化,不再拷贝父线程的inheritableThreadLocals。
最好的解决办法:引入TransmittableThreadLocal,一次性解决所有线程池的上下文传递问题。
最后,如果你正准备升级项目,建议先用压力测试模拟异步场景,确认是否有上下文丢失。也欢迎在评论区分享你升级路上遇到的其他“惊喜”。
最近缺项目经历想快速提升项目实战能力(包含多个AI项目),或者最近找工作,或者想学习AI的小伙伴,可以看看下面👇🏻的这个链接(或许真的能够帮到你)。