← 返回题目列表

@Async 异步的原理是什么?为什么有时会失效?

中等 第 17 / 30 题 更新于 2026/07/26
Async异步线程池AOP

简化版

@Async 让方法异步执行——调用方不用等它执行完,方法会被丢到线程池的另一个线程里跑,调用方立即返回。它的原理和 @Transactional 一样是 AOP 动态代理:Spring 给带 @Async 的 Bean 生成代理,调用被代理的方法时,代理把「真正的方法执行」提交给线程池,自己立即返回。用它要 @EnableAsync 开启。常见失效场景(和事务失效同源):① 自调用(同类内 this 调 @Async 方法,不走代理);② 方法非 public(代理拦不到);③ 没加 @EnableAsync④ 用默认线程池(SimpleAsyncTaskExecutor 不复用线程、来一个建一个,可能耗尽资源,生产必须自定义线程池)。

详细版

基本用法

@EnableAsync   // 启动类或配置类上开启(必须!)
@Configuration
public class AsyncConfig {
    @Bean("myExecutor")   // 强烈建议自定义线程池
    public Executor myExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(1000);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

@Service
public class NotifyService {
    @Async("myExecutor")   // 指定线程池,异步执行
    public void sendSms(String phone) {
        // 这段会在线程池的另一个线程里跑,调用方不阻塞
    }

    @Async
    public CompletableFuture<String> queryAsync() {   // 有返回值用 Future/CompletableFuture
        return CompletableFuture.completedFuture("result");
    }
}

原理

@EnableAsync 注册一个 AsyncAnnotationBeanPostProcessor(BPP)
→ 它给带 @Async 的 Bean 生成 AOP 代理
→ 调用 @Async 方法时,代理不直接执行,而是把方法包装成一个任务提交给线程池
→ 线程池的线程执行真正的方法,调用方的代理立即返回

四大失效场景

失效场景原因解决
自调用(this.asyncMethod())不经过代理,直接调原方法注入自己的代理,或拆到别的 Bean
方法非 public代理无法拦截 private/protected 方法改成 public
没加 @EnableAsync根本没启用异步处理加 @EnableAsync
用默认线程池SimpleAsyncTaskExecutor 不复用线程自定义 ThreadPoolTaskExecutor

⚠️ @Async 不指定线程池时,默认用 SimpleAsyncTaskExecutor——它每次都新建线程、不复用、无队列上限,高并发下会创建海量线程耗尽资源。这和「不要用 Executors 快捷方法」是同一个坑。生产环境必须自定义 ThreadPoolTaskExecutor 并在 @Async("名字") 里指定

完整版教学

一、@Async 的价值:把耗时操作丢到后台

有些操作耗时但不影响主流程结果——发短信、发邮件、记日志、异步计算。如果同步执行,主流程要一直等它完成:

同步:register() → saveUser()(0.1s) → sendSms()(2s) → 返回  总耗时 2.1s,用户干等
异步:register() → saveUser()(0.1s) → sendSms() 丢给线程池 → 立即返回  用户只等 0.1s
      短信在后台线程慢慢发,不阻塞用户

@Async 就是把「不需要等结果的耗时操作」丢到后台线程执行,让主流程快速返回、提升响应速度。它是「进程内异步」的简单方案——比手写 new Thread() 或手动提交线程池优雅(一个注解搞定),比引入消息队列轻量(不用额外中间件)。适合「同一应用内、不需要跨服务、允许失败重试的旁路任务」。

二、原理:又是 AOP 动态代理

@Async 的实现和 @Transactional 如出一辙——都是 AOP 代理。理解了一个就理解了另一个:

@EnableAsync 会注册 AsyncAnnotationBeanPostProcessor(一个 BeanPostProcessor)
  → 它扫描带 @Async 的 Bean,给它生成 AOP 代理
  → 代理拦截 @Async 方法的调用

你调用 notifyService.sendSms():
  实际调的是 代理.sendSms()
    → 代理不直接执行方法体
    → 而是把「执行 sendSms 方法体」这件事包装成一个 Runnable/Callable 任务
    → 提交给线程池(executor.submit(task))
    → 代理立即返回(void 返回 null,或返回一个 Future)
  → 线程池的某个线程真正执行 sendSms 的方法体

关键:代理把「方法调用」转成了「向线程池提交任务」。调用方拿到的是「已经提交」的结果(立即返回),真正的执行发生在线程池的另一个线程里。正因为依赖代理,@Async 的失效场景和 @Transactional 完全一致——凡是「绕过代理」的调用,异步就失效。

三、失效场景一:自调用(最经典)

最常见的失效——同一个类里,一个方法直接调用本类的 @Async 方法

@Service
public class NotifyService {
    public void register() {
        this.sendSms();   // ✗ 自调用!this 是原始对象不是代理,@Async 失效,同步执行了
    }
    @Async
    public void sendSms() { ... }
}

原因:@Async 靠代理生效,但 this.sendSms() 里的 this原始对象(不是代理对象),直接调用原始方法,绕过了代理,异步逻辑(提交线程池)根本没机会执行——于是变成普通的同步调用。

外部调用 notifyService.sendSms():走代理 → 异步 ✓
内部 this.sendSms():this 是原始对象 → 绕过代理 → 同步 ✗

解决:① 把 @Async 方法拆到另一个 Bean 里(跨 Bean 调用走代理);② 注入自己的代理@Autowired NotifyService self; self.sendSms(););③ 用 AopContext.currentProxy()(需开启 exposeProxy)。这和 @Transactional 自调用失效是同一个问题、同样的解法。

四、失效场景二、三:非 public 方法与没开启

另外两个失效场景:

// 失效场景二:方法非 public
@Async
private void sendSms() { }   // ✗ private 方法代理拦不到,@Async 失效
@Async
protected void sendSms() { } // ✗ protected 同样有问题(CGLIB 代理下 protected 勉强可以,但不推荐)
// 正解:@Async 方法必须是 public

// 失效场景三:忘了 @EnableAsync
// 如果启动类/配置类上没有 @EnableAsync,
// AsyncAnnotationBeanPostProcessor 根本不会注册 → @Async 完全无效,全部同步执行

原理:代理只能拦截 public 方法(JDK 动态代理基于接口,接口方法都是 public;CGLIB 基于继承,无法重写 private 方法),所以 @Async 必须加在 public 方法上。而 @EnableAsync 是「总开关」——不加它,Spring 压根不会为 @Async 生成代理,注解形同虚设。这两个是「低级但高频」的失效原因。

五、失效场景四:默认线程池的隐患

即使 @Async 生效了,用默认线程池也是个大坑。不指定 executor 时,Spring 用的默认执行器行为很危险:

@Async 不指定线程池的默认行为:
  Spring 4.1 之前 / 某些配置:用 SimpleAsyncTaskExecutor
  → 它根本不是"池"!每次调用都 new 一个新线程,用完就扔,不复用
  → 高并发下:每个异步调用建一个线程 → 线程数暴涨 → OOM / 资源耗尽

问题本质:和"不要用 Executors.newCachedThreadPool"是同一个坑——
  无界地创建线程/任务,把系统资源吃光

所以生产环境的铁律:必须自定义 ThreadPoolTaskExecutor(设核心/最大线程数、有界队列、拒绝策略、线程命名),并在 @Async("executorName") 里指定用它。这样才能控制线程数、避免资源耗尽、且线程命名便于排查。这个坑和线程池那几题一脉相承——任何「无界创建线程/任务」的机制都要警惕

六、@Async 的返回值与异常处理

@Async 方法的返回值和异常处理有讲究:

返回值:
  void:调用方拿不到任何结果,纯粹的"发射后不管"
  Future<T> / CompletableFuture<T>:调用方能拿到 Future,之后 get() 获取结果
  返回普通值(如 String):无意义!因为异步,返回时方法还没执行完,拿到的是 null

异常处理:
  void 方法抛异常:调用方感知不到(在别的线程)!异常会被吞掉
    → 需要自定义 AsyncUncaughtExceptionHandler 来捕获记录
  Future 方法抛异常:异常包在 Future 里,调用方 get() 时抛出

关键坑:@Async 的 void 方法抛异常,调用方完全不知道(异常在异步线程里,不会传回调用方),默认会被静默吞掉。所以要么用 CompletableFuture 返回(异常能通过 get 感知),要么实现 AsyncUncaughtExceptionHandler(配合 AsyncConfigurer)统一处理异步异常。否则异步任务失败了你都发现不了——这是异步编程的通用陷阱。

记忆钩子:「@Async = @EnableAsync + AOP 代理 + 提交线程池;四大失效:自调用(绕过代理)、非 public(拦不到)、没 @EnableAsync(没开)、默认线程池(不复用会 OOM);void 方法异常会被吞,用 CompletableFuture 或 AsyncUncaughtExceptionHandler」

七、常见误区与追问

  • 误区:@Async 加了就一定异步。 有四大失效场景——自调用、非 public 方法、没加 @EnableAsync、(虽异步但)用默认线程池有 OOM 风险;任一都可能导致失效或事故。
  • 误区:@Async 自调用也能异步。 自调用(this.method())绕过代理,退化成同步执行;和 @Transactional 自调用失效同理。
  • 误区:@Async void 方法抛的异常能被调用方捕获。 不能,异常在异步线程里、会被静默吞掉;要用 CompletableFuture 或 AsyncUncaughtExceptionHandler 处理。
  • 误区:@Async 不指定线程池也没事。 默认可能用 SimpleAsyncTaskExecutor(不复用线程、无界建线程),高并发下会耗尽资源;生产必须自定义 ThreadPoolTaskExecutor。
  • 追问:@Async 和 @Transactional 的失效场景为什么这么像? 因为两者都基于 AOP 代理实现,凡是「绕过代理」的调用(自调用、非 public 方法、没开启)都会失效,是同一套原理的表现。
  • 追问:@Async 方法怎么拿返回值?CompletableFuture<T>Future<T> 作返回值,调用方拿到 Future 后 get() 获取结果;返回普通值无意义(异步返回时方法还没执行完)。
  • 追问:@Async 异步线程里能拿到原线程的事务/ThreadLocal 吗? 不能,异步在新线程执行,拿不到原线程 ThreadLocal 里的事务连接和上下文——这也是「事务/上下文在 @Async 里丢失」的原因。

八、加强记忆

@Async 让方法异步执行——调用被丢到线程池的另一个线程跑,调用方立即返回,用于「发短信、记日志」等不需等结果的耗时旁路,提升响应速度。原理和 @Transactional 一模一样是 AOP 代理@EnableAsync 注册一个 BeanPostProcessor 给 @Async Bean 生成代理,代理把「方法执行」包装成任务提交给线程池、自己立即返回。正因依赖代理,四大失效场景和事务同源:① 自调用(this 是原始对象、绕过代理、退化同步)、② 方法非 public(代理拦不到)、③ 没加 @EnableAsync(总开关没开)、④ 用默认线程池(SimpleAsyncTaskExecutor 不复用线程、无界建线程会 OOM,生产必须自定义 ThreadPoolTaskExecutor 并 @Async("名字") 指定)。两个额外陷阱:void 方法抛异常会被静默吞掉(用 CompletableFuture 返回或配 AsyncUncaughtExceptionHandler)、异步线程拿不到原线程的事务和 ThreadLocal。一句话「@EnableAsync + AOP 代理提交线程池,自调用/非 public/没开启/默认池四大失效,void 异常会吞、生产必自定义线程池」。