目录
- 概述
- 核心原理:基于 Spring AOP 的代理机制
- @EnableAsync 与 @Async 的工作流程
- 线程池(TaskExecutor)的选择与原理
- 返回值处理:Future / CompletableFuture
- 异常处理机制
- 常见陷阱与失效场景
- 完整示例代码
- 与其他并发方案的对比
- 生产环境最佳实践
1. 概述
Spring Boot 的异步并发能力主要通过 @EnableAsync + @Async 注解实现。其本质是:
Spring 在 Bean 初始化时,对带有
@Async注解的方法所属的类生成一个动态代理,方法被调用时不直接执行业务逻辑,而是将其包装成一个Runnable/Callable任务,提交给线程池(TaskExecutor)异步执行。
整个过程涉及到三个关键技术点:
- Spring AOP / 动态代理(JDK Proxy 或 CGLIB)
- BeanPostProcessor 机制(
AsyncAnnotationBeanPostProcessor) - JUC 线程池(
ThreadPoolTaskExecutor包装的ThreadPoolExecutor)
2. 核心原理:基于 Spring AOP 的代理机制
2.1 整体架构图
┌─────────────────────────────────────────────────────────────┐
│ Caller (调用者) │
└──────────────────────────┬──────────────────────────────────┘
│ 调用 service.doAsync()
▼
┌─────────────────────────────────────────────────────────────┐
│ Proxy (代理对象,由 Spring 容器返回) │
│ - JDK Dynamic Proxy(接口) │
│ - CGLIB Proxy(无接口) │
└──────────────────────────┬──────────────────────────────────┘
│ 拦截方法调用
▼
┌─────────────────────────────────────────────────────────────┐
│ AnnotationAsyncExecutionInterceptor │
│ (MethodInterceptor 拦截器) │
│ 1. 解析 @Async 注解值,确定要使用的 Executor │
│ 2. 将方法封装为 Callable<T> │
│ 3. 提交到 TaskExecutor │
│ 4. 立即返回 Future / CompletableFuture / null │
└──────────────────────────┬──────────────────────────────────┘
│ executor.submit(task)
▼
┌─────────────────────────────────────────────────────────────┐
│ ThreadPoolTaskExecutor (线程池) │
│ ├─ 核心线程数 corePoolSize │
│ ├─ 最大线程数 maxPoolSize │
│ ├─ 任务队列 queueCapacity │
│ └─ 拒绝策略 RejectedExecutionHandler │
└──────────────────────────┬──────────────────────────────────┘
▼
目标方法在工作线程中执行
2.2 关键类一览
| 类/接口 | 作用 |
|---|---|
@EnableAsync | 通过 @Import(AsyncConfigurationSelector.class) 引入异步配置 |
AsyncConfigurationSelector | 根据 AdviceMode 选择 Proxy 或 AspectJ 模式 |
ProxyAsyncConfiguration | 注册 AsyncAnnotationBeanPostProcessor |
AsyncAnnotationBeanPostProcessor | BeanPostProcessor,扫描 Bean 并织入 Advisor |
AsyncAnnotationAdvisor | 切面定义:Pointcut(@Async) + Advice(拦截器) |
AnnotationAsyncExecutionInterceptor | 真正的拦截逻辑:包装任务并提交到 Executor |
TaskExecutor / AsyncTaskExecutor | Spring 抽象的执行器接口 |
ThreadPoolTaskExecutor | Spring 对 java.util.concurrent.ThreadPoolExecutor 的包装 |
3. @EnableAsync 与 @Async 的工作流程
3.1 启动阶段(Bean 容器初始化)
Spring Boot 启动
│
▼
解析 @EnableAsync 注解
│
▼
@Import(AsyncConfigurationSelector.class)
│
▼
注册 ProxyAsyncConfiguration 配置类
│
▼
创建 AsyncAnnotationBeanPostProcessor (单例)
│
▼
对每个 Bean 调用 postProcessAfterInitialization()
│
├─ 检查类或方法是否有 @Async
│
├─ 命中 → 使用 ProxyFactory 创建代理对象
│ 织入 AsyncAnnotationAdvisor
│
└─ 未命中 → 返回原 Bean
3.2 运行阶段(方法调用)
AnnotationAsyncExecutionInterceptor#invoke 的核心伪代码:
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. 找到 @Async 标注的方法应使用哪个 Executor
AsyncTaskExecutor executor = determineAsyncExecutor(method);
// 2. 把目标方法包装成 Callable
Callable<Object> task = () -> {
try {
Object result = invocation.proceed();
if (result instanceof Future) {
return ((Future<?>) result).get();
}
return result;
} catch (Throwable ex) {
handleError(ex, method, invocation.getArguments());
throw ex;
}
};
// 3. 根据返回类型分发到不同的提交方式
return doSubmit(task, executor, method.getReturnType());
}
doSubmit 的分发逻辑:
| 方法返回类型 | 提交方式 | 调用方拿到的结果 |
|---|---|---|
void | executor.submit(Runnable) | 立即返回 null |
Future<T> | executor.submit(Callable<T>) | FutureTask<T> |
CompletableFuture<T> | CompletableFuture.supplyAsync(..., executor) | CompletableFuture<T> |
ListenableFuture<T> | executor.submitListenable(...) | ListenableFuture<T> |
4. 线程池(TaskExecutor)的选择与原理
4.1 默认行为(Spring Boot 2.1+)
Spring Boot 在 TaskExecutionAutoConfiguration 中自动装配了一个名为 applicationTaskExecutor 的 ThreadPoolTaskExecutor。
默认参数(可通过 spring.task.execution.* 配置):
spring:
task:
execution:
pool:
core-size: 8 # 核心线程数(默认 8)
max-size: Integer.MAX # 最大线程数(默认无界,注意!)
queue-capacity: Integer.MAX # 队列容量(默认无界)
keep-alive: 60s
thread-name-prefix: task-
⚠️ 注意:默认队列是无界的,意味着 maxPoolSize 永远不会被触发——这是生产事故的重灾区。
4.2 Executor 选择优先级
@Async 拦截器选择 Executor 的顺序:
1. @Async("beanName") 中显式指定的名字
│ 找不到 ↓
2. 实现了 AsyncConfigurer.getAsyncExecutor() 的 Bean
│ 找不到 ↓
3. 容器中类型为 TaskExecutor 的唯一 Bean
│ 找不到 ↓
4. 名字为 "taskExecutor" 的 Executor Bean
│ 找不到 ↓
5. SimpleAsyncTaskExecutor(每次都新建线程,⚠️ 不要用于生产)
4.3 ThreadPoolTaskExecutor 的内核
ThreadPoolTaskExecutor 内部封装的就是 JUC 的 ThreadPoolExecutor:
new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveSeconds, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
threadFactory,
rejectedExecutionHandler
);
任务提交时的 JUC 经典流程:
submit(task)
│
▼
当前线程数 < corePoolSize ? ──是──► 创建新核心线程执行
│否
▼
任务队列未满 ? ──────────────是──► 入队等待
│否
▼
当前线程数 < maxPoolSize ? ───是──► 创建新非核心线程执行
│否
▼
执行 RejectedExecutionHandler(默认 AbortPolicy 抛异常)
5. 返回值处理:Future / CompletableFuture
5.1 三种典型用法
// 1) fire-and-forget:不关心结果
@Async
public void sendEmail(String to) { ... }
// 2) Future:阻塞式获取结果(不推荐,已过时风格)
@Async
public Future<String> oldStyle() {
return new AsyncResult<>("done");
}
// 3) CompletableFuture:现代异步编排(推荐)
@Async
public CompletableFuture<User> loadUser(Long id) {
return CompletableFuture.completedFuture(repo.findById(id));
}
5.2 CompletableFuture 的优势
可以做非阻塞编排:
CompletableFuture<User> userF = service.loadUser(1L);
CompletableFuture<List<Order>> ordersF = service.loadOrders(1L);
userF.thenCombine(ordersF, (user, orders) -> buildView(user, orders))
.thenAccept(view -> render(view))
.exceptionally(ex -> { log.error("err", ex); return null; });
5.3 原理细节:CompletableFuture 是怎么"装上"线程池的?
当 @Async 方法返回 CompletableFuture<T> 时,拦截器实际上做的是:
CompletableFuture.supplyAsync(() -> {
// 调用真实方法,拿到内部 CompletableFuture,再 join 出值
CompletableFuture<T> inner = (CompletableFuture<T>) invocation.proceed();
return inner.join();
}, asyncExecutor);
所以整个异步链路使用的就是 @Async 配置的那个线程池,不会跑到 ForkJoinPool.commonPool 上去(这是没有 @Async 时直接用 CompletableFuture.supplyAsync 的默认行为)。
6. 异常处理机制
6.1 异常如何"消失"
当 @Async 方法返回 void:
- 异常不会抛回调用方(因为调用方早已返回)
- 异常会被
AsyncUncaughtExceptionHandler捕获 - 默认实现
SimpleAsyncUncaughtExceptionHandler仅打日志
当返回 Future / CompletableFuture:
- 异常封装在 Future 中,调用
.get()时抛出ExecutionException - 或通过
.exceptionally()/.handle()处理
6.2 自定义全局异常处理器
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(8);
exec.setMaxPoolSize(32);
exec.setQueueCapacity(200);
exec.setThreadNamePrefix("async-");
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
exec.initialize();
return exec;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Async method {} failed, params={}", method, params, ex);
// 上报监控、报警等
};
}
}
7. 常见陷阱与失效场景
7.1 自调用失效(最经典的坑)
@Service
public class FooService {
public void a() {
b(); // ❌ this.b(),绕过代理,@Async 失效!
}
@Async
public void b() { ... }
}
原因:@Async 依赖 AOP 代理,this.b() 是直接走对象引用,不经过代理。
解决方案:
- 注入自己(不推荐,循环依赖隐患)
- 拆分到另一个 Bean
- 使用
AopContext.currentProxy()(需要@EnableAspectJAutoProxy(exposeProxy = true))
7.2 方法不是 public
JDK 动态代理只能代理接口的 public 方法;CGLIB 不能代理 final / static / private 方法。
7.3 在 @PostConstruct 中调用 @Async 方法
@PostConstruct 执行时,BeanPostProcessor 还没完成代理织入,调用 @Async 方法不会异步。
7.4 默认线程池无界队列导致 OOM
前面提过:默认 queue-capacity 是 Integer.MAX_VALUE,任务永远入队,maxPoolSize 形同虚设,最终 OOM。生产环境必须显式配置。
7.5 上下文丢失
异步线程拿不到主线程的:
RequestContextHolder(ServletRequest)SecurityContextHolder(Spring Security)ThreadLocal(MDC 日志、租户上下文)
解决方案:
RequestContextFilter设置threadContextInheritable=trueSecurityContextHolder.MODE_INHERITABLETHREADLOCAL- 包装
TaskDecorator在任务执行前手动复制 ThreadLocal:
exec.setTaskDecorator(runnable -> {
Map<String,String> mdc = MDC.getCopyOfContextMap();
RequestAttributes attrs = RequestContextHolder.currentRequestAttributes();
return () -> {
try {
if (mdc != null) MDC.setContextMap(mdc);
RequestContextHolder.setRequestAttributes(attrs);
runnable.run();
} finally {
MDC.clear();
RequestContextHolder.resetRequestAttributes();
}
};
});
7.6 事务传播失效
@Async 方法会运行在新线程上,与调用方的事务完全隔离。@Transactional 在 @Async 方法内仍然有效,但属于一个独立事务。
8. 完整示例代码
8.1 启用与配置
@SpringBootApplication
@EnableAsync
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
@Configuration
public class ExecutorConfig {
@Bean("ioExecutor")
public Executor ioExecutor() {
ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
e.setCorePoolSize(16);
e.setMaxPoolSize(64);
e.setQueueCapacity(500);
e.setKeepAliveSeconds(60);
e.setThreadNamePrefix("io-");
e.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
e.setWaitForTasksToCompleteOnShutdown(true);
e.setAwaitTerminationSeconds(30);
e.initialize();
return e;
}
@Bean("cpuExecutor")
public Executor cpuExecutor() {
ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
int cores = Runtime.getRuntime().availableProcessors();
e.setCorePoolSize(cores);
e.setMaxPoolSize(cores);
e.setQueueCapacity(1000);
e.setThreadNamePrefix("cpu-");
e.initialize();
return e;
}
}
8.2 使用
@Service
public class OrderService {
@Async("ioExecutor")
public CompletableFuture<Order> fetchOrder(Long id) {
// 模拟远程调用
return CompletableFuture.completedFuture(remote.get(id));
}
@Async("ioExecutor")
public CompletableFuture<User> fetchUser(Long id) {
return CompletableFuture.completedFuture(remote.getUser(id));
}
}
@RestController
public class OrderController {
@Autowired OrderService svc;
@GetMapping("/detail/{id}")
public CompletableFuture<Detail> detail(@PathVariable Long id) {
return svc.fetchOrder(id)
.thenCombine(svc.fetchUser(id), Detail::new);
// Spring MVC 自动支持 CompletableFuture 返回,
// 底层走 Servlet 3.1 异步 (AsyncContext),释放 Tomcat 工作线程
}
}
9. 与其他并发方案的对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
@Async + 线程池 | 业务级异步、解耦 | 声明式、无侵入 | AOP 限制、上下文丢失 |
CompletableFuture 直接用 | 同方法内并发编排 | 灵活、链式 | 需手动管理线程池 |
Reactor (Mono/Flux) | 全栈响应式、高并发 IO | 背压、非阻塞 | 学习成本高、调试难 |
| 虚拟线程 (Loom, Java 21+) | 高并发同步代码 | 写法简单、可大量并发 | 需要 JDK 21+ |
@Scheduled | 定时任务 | 简单 | 不是为业务并发设计 |
| MQ (Kafka/RabbitMQ) | 跨进程异步、可靠投递 | 解耦、可重放 | 引入中间件 |
10. 生产环境最佳实践
- 永远显式配置线程池,不要用默认的无界队列。
- 按场景拆分线程池:IO 密集、CPU 密集、定时任务各一个,避免互相饿死。
- 设置合理的拒绝策略:
CallerRunsPolicy提供背压;AbortPolicy快速失败。 - 启用线程池监控:暴露
activeCount、queueSize、completedTaskCount到 Prometheus。 - 优雅停机:
setWaitForTasksToCompleteOnShutdown(true)+setAwaitTerminationSeconds(N),避免 Pod 重启时丢任务。 - 传递上下文:用
TaskDecorator复制 MDC、TraceId、Security、Tenant。 - 避免在
@Async内部依赖调用方事务/请求,把它当作独立的执行单元。 - 优先返回
CompletableFuture而不是void或老式Future。 - 结合 Spring MVC 的 Servlet 3 异步,让 Controller 直接返回
CompletableFuture,在等待期间释放 Tomcat 线程。 - JDK 21+ 可考虑虚拟线程:
spring.threads.virtual.enabled=true直接把 Tomcat 与@Async切到虚拟线程,往往是更优解。
一句话总结
Spring Boot 的
@Async= AOP 代理拦截方法调用 + 把方法包装成任务 + 丢给ThreadPoolTaskExecutor执行 + 用Future/CompletableFuture把结果送回调用方。理解 AOP 代理与 JUC 线程池这两个底座,所有"异步失效""任务积压""上下文丢失"的问题就都能自洽解释。