bigywater.tech
Spring · 24

@Async:线程池、异常去向与上下文传播

@Async 到底用的是什么线程池?异常去哪了?ThreadLocal、MDC 能不能带过去?线程池满了会怎样?这篇在 Spring Framework 7.0.9 和 Spring Boot 4.0.8 上逐项验证。

100 对 8
纯 Spring 默认与 Spring Boot 默认下,100 次 @Async 调用用到的线程数
6 个
核心 2、队列 2 的执行器同时收到 10 个任务时被拒绝的个数(默认拒绝策略)
null
没有 TaskDecorator 时异步线程里读到的 ThreadLocal 值

默认执行器

实测 · 本机执行输出
  纯 Spring,@EnableAsync 没有配置执行器:100 次 @Async 调用(每次 30 ms)用了 100 个不同的线程,线程名举例 [SimpleAsyncTaskExecutor-1, SimpleAsyncTaskExecutor-10]
  Spring Boot(@EnableAutoConfiguration + @EnableAsync,没有配执行器):100 次调用用了 8 个线程 [task-1, task-2, task-3, task-4, task-5, task-6, task-7, task-8];容器里的 TaskExecutor Bean:[applicationTaskExecutor]
  配了 ThreadPoolTaskExecutor(核心 4):100 次调用用了 4 个线程 [pool-1, pool-2, pool-3, pool-4];完成 100 个

纯 Spring 每次新建线程,Boot 配了带 8 个核心线程的 applicationTaskExecutor。

异常

实测 · 本机执行输出
  (a)默认:
  LOG ERROR SimpleAsyncUncaughtExceptionHandler: Unexpected exception occurred invoking async method: public void demo.Main$Work.boomVoid()
    boomFuture() 返回的 Future:get() 抛 ExecutionException ← IllegalStateException: Future 方法里的异常
    returnsString()(@Async 但返回 String)→ IllegalArgumentException: Invalid return type for async method (only Future and void supported): class java.lang.String
  (b)自定义处理器:
    自定义 AsyncUncaughtExceptionHandler 收到:void 方法里的异常,方法 boomVoid
    boomFuture() 返回的 Future:get() 抛 ExecutionException ← IllegalStateException: Future 方法里的异常
    returnsString()(@Async 但返回 String)→ IllegalArgumentException: Invalid return type for async method (only Future and void supported): class java.lang.String

void 方法的异常默认只记日志,Future 方法的异常放进 Future,返回别的类型直接抛异常。

上下文

实测 · 本机执行输出
    没有 TaskDecorator:提交线程 ThreadLocal = 请求-42;异步线程里读到 → 来自 pool-1,ThreadLocal=null
    加了 TaskDecorator:提交线程 ThreadLocal = 请求-42;异步线程里读到 → 来自 deco-1,ThreadLocal=请求-42

ThreadLocal 不跨线程,用 TaskDecorator 传递并记得清理。

线程池满了

实测 · 本机执行输出
    核心 2、最大 2、队列 2、默认拒绝策略,提交 10 个各 300 ms 的任务:提交本身花了 3 ms;被拒绝 6 个 [TaskRejectedException];最终完成 4 个;执行过的线程 [sat-1, sat-2]
    核心 2、最大 2、队列 2、CallerRunsPolicy,提交 10 个各 300 ms 的任务:提交本身花了 915 ms;被拒绝 0 个 [];最终完成 10 个;执行过的线程 [main, sat-1, sat-2]

默认抛 TaskRejectedException,CallerRunsPolicy 让调用者自己执行。

内部调用

实测 · 本机执行输出
    从外部调用 @Async 方法 who():执行线程 [pool-1];callInternal() 里调 this.fire(0) 的调用者线程 main(内部调用不异步,见自调用篇)

this 调用 @Async 方法是同步的。

分步演示

@Async
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。第一节纯 Spring 的两条用不带 Boot 的 Spring Framework 7.0.9,Boot 那条用 Spring Boot 4.0.8 的 @EnableAutoConfiguration + @EnableAsync;提交耗时和线程名每次运行略有不同(CallerRuns 的提交耗时在 900 ms 多一点)
    • 没有验证:Boot 的 spring.task.execution.* 配置项;虚拟线程执行器(见 MVC 篇);@Async 与 @Transactional 组合时的事务边界(见事务篇);AsyncConfigurer 与 @Bean TaskExecutor 同时存在时的选择;@Async("beanName") 指定执行器;CompletableFuture 的异常传播细节;线程池 shutdown 时未完成任务的处理
    京ICP备2026067502号-1京公网安备11010502063487号