@async不指定线程池会默认使用simpleasynctaskexecutor,每次调用新建线程、不复用、无上限、不回收,高并发下易导致oom或cpu打满;必须显式配置threadpooltaskexecutor并合理设置核心参数。

@Async 不加线程池名会掉进 SimpleAsyncTaskExecutor 陷阱
不指定线程池时,@Async 默认走 SimpleAsyncTaskExecutor,它每次调用都新建线程,不复用、无上限、不回收。高并发下可能瞬间创建数百线程,OOM 或 CPU 打满是常态。
典型症状包括:日志里线程名全是 SimpleAsyncTaskExecutor-1、SimpleAsyncTaskExecutor-2 这类无规律命名;服务压测后响应延迟飙升但 CPU 使用率忽高忽低;JVM 线程数持续上涨停不下来。
必须显式指定线程池 Bean 名,例如:@Async("taskExecutor")。这个名称要和 @Bean 方法上标注的 name 严格一致,大小写敏感。
ThreadPoolTaskExecutor 的核心参数不是拍脑袋定的
corePoolSize、maxPoolSize、queueCapacity 三者协同决定吞吐与稳定性,不能孤立设置:
-
corePoolSize建议设为Runtime.getRuntime().availableProcessors(),CPU 密集型任务可略高,IO 密集型可设为 2–4 倍 -
maxPoolSize通常为corePoolSize × 2,避免突发流量直接打爆 -
queueCapacity不宜过大(比如设成Integer.MAX_VALUE),否则任务全堆积在队列里,主线程以为“已提交”实则“未执行”,监控失真 - 务必设置
setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()),防止队列满时任务静默丢弃
@Async 方法必须满足“代理可见性”才能生效
@Async 是基于 Spring AOP 代理实现的,因此有硬性限制:
- 方法不能是
private、static或final - 不能在同一个类内自调用(比如
methodA()调this.methodB()),因为代理对象没介入 - 目标类必须是 Spring 管理的 Bean(即被
@Service、@Component等注解标记) - 若使用 JDK 动态代理(接口代理),方法需定义在接口中;CGLIB 代理则无此要求,但类不能是
final
最稳妥的做法:把异步逻辑抽到独立 @Service 类中,用 @Autowired 注入调用。
CompletableFuture.runAsync() 和 @Async 不是替代关系,而是互补
当需要链式编排、异常捕获、结果聚合时,@Async 就力不从心了。此时应切换到 CompletableFuture,但它仍依赖线程池:
- 用
CompletableFuture.runAsync(Runnable, Executor)显式传入ThreadPoolTaskExecutorBean,避免走 ForkJoinPool.commonPool() - 不要混用
@Async和CompletableFuture包裹同一段逻辑,容易造成线程上下文丢失(如SecurityContext、RequestContextHolder) - 若需传递 MDC 日志上下文,得手动在
runAsync内部做MDC.getCopyOfContextMap()+MDC.setContextMap(),@Async同理
真正难处理的从来不是怎么起线程,而是线程间上下文传递、错误传播、超时控制和可观测性——这些点一旦漏掉,异步就从解耦变成埋雷。










