@Async 注解的原理是什么?
- 标签:后端、Spring
🏛 业务架构师
@Async 的原理一句话:基于 AOP 动态代理 + 线程池异步执行。
机制拆解:
@EnableAsync向容器注册AsyncAnnotationBeanPostProcessor;- 它在 Bean 初始化后为含
@Async的类生成代理(和事务代理同一套体系); - 调用方拿到的是代理对象,调用异步方法时代理拦截,把方法体包装成任务提交到配置的线程池(默认
SimpleAsyncTaskExecutor——注意它不复用线程,生产必须自定义 ThreadPoolTaskExecutor); - 调用方立即返回,方法在线程池线程里执行,返回值用
Future/CompletableFuture承载。
架构师视角,我关注异步背后的业务风险清单:
- 线程池必须有边界:默认执行器线程不复用,高并发下线程暴涨。核心线程数、队列容量、拒绝策略(CallerRuns 是背压兜底)都要按业务容量规划,并接入监控;
- 上下文会丢:SecurityContext、traceId、租户标识基于 ThreadLocal,换线程即失效——要么装饰器传递(TaskDecorator),要么显式传参,否则异步日志断链、权限丢失;
- 异常静默:异步方法的异常不会传给调用方,必须配
AsyncUncaughtExceptionHandler和兜底告警; - 可靠性边界:进程内异步"应用重启任务即丢"。发券、扣积分这类不能丢的业务动作要落库 + 重试/MQ,@Async 只适合"丢了可接受"(日志、通知类)的场景。
一句话:@Async 是吞吐优化工具,不是可靠性工具——业务上先问"这个任务能不能丢",再决定用不用它。
🏗 系统架构师
机制三句话:
@EnableAsync开启后,AsyncAnnotationBeanPostProcessor 为含@Async的 Bean 生成代理;- 方法被调用时代理拦截,把方法体包装成任务提交到 TaskExecutor 线程池立即返回——无返回值返回 void,有则返回
Future/CompletableFuture; - 本质是 AOP 代理模式,因此继承代理的全部边界:自调用不走代理即失效、必须 public、final 方法无效。
架构师关注的三件事:
- 线程池治理是重点而不是注解:必须按业务隔离线程池(IO 型与通知型分开),显式设置核心数、有界队列、拒绝策略——线程池配置失当的
@Async就是定时炸弹; - 上下文传播:MDC 的 traceId、SecurityContext 默认不跨线程,需 TaskDecorator 复制——否则异步日志全部断链;
- 虚拟线程的替代关系:JDK 21 加 Boot 3.2 后,简单异步场景可由虚拟线程承担(
spring.threads.virtual.enabled=true),@Async保留给需要池化隔离与背压控制的场景。
💻 全栈开发专家
本质是 AOP 动态代理 + 线程池:
@EnableAsync导入AsyncAnnotationBeanPostProcessor,给带@Async的 Bean 生成代理- 调用进入代理方法,切面
AnnotationAsyncExecutionInterceptor#invoke拦截 - 选线程池:先取
@Async("xxx")指定的池 → 唯一的TaskExecutorBean → 名为taskExecutor的 Bean → 都没有就用SimpleAsyncTaskExecutor(每次新建线程不复用,纯 Spring 环境的默认坑) - 提交任务执行,返回值必须
Future/CompletableFuture才拿得到结果和异常
java
@Async("bizExecutor")
public CompletableFuture<Boolean> syncToErp(Order order) { ... }Boot 环境默认池是 applicationTaskExecutor(核心 8、队列无界),IO 密集任务全挤 8 个线程会饿死——正式项目我都显式定义池并指定队列大小、拒绝策略(CallerRuns 兜底),再用 Micrometer 把池指标接进 Prometheus 观察活跃度和拒绝数。
另外子线程拿不到 MDC 的 traceId 和 SecurityContext,要 TaskDecorator 显式传递。排查异步没生效,最简单一招:方法里打 Thread.currentThread().getName(),看是不是还在 http-nio-exec 线程里。