@Async 加了没生效?Spring Boot 异步编程全链路拆解:从线程池到自调用陷阱
为什么你加了 @Async 方法还是同步执行?为什么异步方法一调用就报"找不到代理"?为什么线上偶发丢消息?这一篇从注解解析、代理生成、线程池选择到三大经典坑位,把 Spring Boot 异步全链路一次拆透。
一、@Async 到底干了什么?
先看最基础的用法:
java
@Service
public class OrderService {
@Async
public void sendNotification(Long orderId) {
// 模拟发短信、推送
System.out.println(Thread.currentThread().getName()
+ " 发送通知: " + orderId);
}
}
调用方:
java
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/order")
public String create(@RequestBody OrderDTO dto) {
orderService.sendNotification(dto.getOrderId()); // 立即返回,不阻塞
return "下单成功";
}
}
如果你以为"加个注解就异步了",那说明你还没见过它翻车的样子。下面从原理拆起。
二、@Async 的执行原理:三步走
2.1 注解解析:AsyncAnnotationBeanPostProcessor
Spring 启动时,@EnableAsync 会注册一个 AsyncAnnotationBeanPostProcessor(Bean 后置处理器)。它在每个 Bean 实例化后检查:这个类上有没有 @Async 标注的方法?
swift
Bean 实例化
│
▼
AsyncAnnotationBeanPostProcessor.postProcessAfterInitialization
│
├─ 扫描类中所有方法
├─ 找到带 @Async 的方法
└─ 生成 CGLIB 代理,拦截该方法的调用
2.2 代理生成:CGLIB 动态代理
Spring 发现异步方法后,会为 Bean 生成一个 CGLIB 代理对象 。调用 orderService.sendNotification() 时,实际执行的是代理对象的拦截逻辑,而不是原始方法。
scss
调用方 ──► orderService(代理对象)
│
├─ 拦截器 AsyncExecutionInterceptor
│ │
│ ├─ 从线程池取线程(或创建)
│ ├─ 提交任务到线程池
│ └─ 立即返回(调用方不阻塞)
│
└─► 真实 TaskService.sendNotification()
在线程池中的某个线程上执行
关键点:代理对象是"包装"在 Bean 外面的。如果你拿到的是原始 Bean(未被代理),那 @Async 就是无效的。
2.3 线程池选择:三优先
AsyncExecutionInterceptor 选择线程池的顺序:
| 优先级 | 方式 | 说明 |
|---|---|---|
| 1 | @Async("taskExecutor") 显式指定 |
方法上的 value 指定 Bean 名 |
| 2 | 自定义 TaskExecutor Bean |
容器中唯一的一个 |
| 3 | SimpleAsyncTaskExecutor |
兜底:每个任务新建线程,无复用 |
很多人踩坑就在这:没有配置线程池,Spring 用 SimpleAsyncTaskExecutor 兜底,每次异步调用都 new 一个线程,高并发下直接打爆。
三、常见坑位:为什么 @Async 不生效?
坑位 1:自调用(this 调用)
同一个类内部调用异步方法,走的是 this.xxx(),不会经过代理对象,@Async 直接失效。
java
@Service
public class OrderService {
// 坑!这个方法里调用自身的异步方法,不会生效
public void handle(Order order) {
sendNotification(order.getId()); // this.xxx() 非代理调用
}
@Async
public void sendNotification(Long orderId) { ... }
}
解法 :把异步方法放到独立的 Service,从外部调用;或注入自身代理(@Lazy 注入 self)。
java
@Service
public class OrderService {
@Autowired
@Lazy
private OrderService self; // 注入的是代理
public void handle(Order order) {
self.sendNotification(order.getId()); // 走代理,生效
}
}
坑位 2:方法可见性
@Async 方法不能是 private,也不能是 final------CGLIB 代理无法覆盖它们。protected / public 都可以。
| 修饰符 | 是否生效 | 原因 |
|---|---|---|
| public | ✅ | 代理可拦截 |
| protected | ✅(一般) | 可拦截 |
| private | ❌ | 代理无法重写 |
| final | ❌ | 代理无法继承重写 |
坑位 3:返回值类型
@Async 方法只能返回 void 或 Future(CompletableFuture、ListenableFuture)。返回普通对象会被忽略,甚至报错。
java
@Async
public CompletableFuture<String> queryInfo(Long id) {
return CompletableFuture.completedFuture("结果");
}
坑位 4:异常吞掉
异步方法里的异常,不会抛给调用方。如果不在异步方法内 try-catch,异常静默丢失,排错困难。
java
@Async
public void send(String msg) {
try {
// 业务
} catch (Exception e) {
log.error("异步任务失败", e); // 必须自己记录
}
}
四、正确姿势:自定义线程池 + 优雅配置
生产环境建议这样配:
java
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Bean("bizExecutor")
public ThreadPoolTaskExecutor bizExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("biz-async-");
// 关键:拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
CallerRunsPolicy在队列满时让调用线程执行任务------牺牲一点性能,保证任务不丢失。
五、异步 + 事务:一对冤家
@Async 和 @Transactional 叠加时要小心 :事务是绑定在线程上的(ThreadLocal)。@Async 把方法丢到另一个线程执行,事务上下文不传递,常见坑:异步方法里的事务不生效。
java
@Async
@Transactional
public void doAsyncWithTx() {
// ⚠️ 事务和异步都在代理层,但异步线程上没有事务绑定
// 更安全的做法:异步方法内部手动开启事务
}
解法:
- 异步方法里用
TransactionTemplate手动控制; - 或者先同步提交事务,再异步执行下游动作。
六、异步编排:CompletableFuture 的花式玩法
@Async 配合 CompletableFuture 可以玩出很多编排:
java
@Async("bizExecutor")
public CompletableFuture<Integer> getA() { ... }
@Async("bizExecutor")
public CompletableFuture<Integer> getB() { ... }
// 并行执行,聚合结果
CompletableFuture.allOf(fa, fb).join();
int sum = fa.get() + fb.get();
七、@Async 全链路一张图
scss
HTTP 请求线程 ──► OrderController.create()
│
▼
orderService (CGLIB 代理)
│
├─ 同步方法 → 原线程执行
│
└─ 异步方法 → AsyncExecutionInterceptor
│
▼
线程池 ThreadPoolTaskExecutor
┌─────┬─────┬─────┬─────┐
│ t1 │ t2 │ t3 │ t4 │ ← 异步线程
└─────┴─────┴─────┴─────┘
│
▼
TaskService.sendNotification()
异常 → 线程池拒绝策略 或 静默丢失
八、面试高频点总结
| 问题 | 答案 |
|---|---|
| @Async 生效的前提 | 方法必须通过代理调用(外部调用) |
| 线程池怎么选 | @Async("beanName") > 唯一TaskExecutor > SimpleAsyncTaskExecutor |
| 为什么自调用不生效 | this 调用不经过代理 |
| 事务能跨线程吗 | 不能,ThreadLocal 绑定线程 |
| 异步异常怎么处理 | 方法内 try-catch,或 CompletableFuture.exceptionally |
九、总结
@Async 是一个"看着简单、用着翻车"的注解。核心记住三点:
- 代理是关键:必须从外部走代理调用才生效
- 线程池必须显式配置:别用 SimpleAsyncTaskExecutor 兜底
- 异常要自己管:异步方法里一定要记录异常