Spring @Async 不生效排查:自调用失效、默认线程池坑与异步方法里的异常去哪了

Spring @Async 不生效排查:自调用失效、默认线程池坑与异步方法里的异常去哪了

给一个耗时方法加上 @Async,满心以为它会丢到别的线程异步跑,结果日志一看------还是主线程串行执行,该慢还是慢。@Async 是 Spring 里「看着简单、坑却密集」的注解之一:它靠 AOP 代理实现,一旦触发代理失效的场景就悄无声息地退回同步执行,不报错、不警告,极难发现。

这篇把 @Async 不生效的几类原因、默认线程池的隐患、以及异步方法里异常「凭空消失」的问题一次讲清,每个都给可复现的代码和修法。

先让 @Async 能用起来:两件事缺一不可

@Async 生效有两个前提:主类(或某个配置类)上加 @EnableAsync,方法上加 @Async。少任何一个都不异步。

java 复制代码
@SpringBootApplication
@EnableAsync   // 少了这个,@Async 完全被无视
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}
java 复制代码
@Service
public class MailService {

    @Async
    public void sendMail(String to) {
        // 打印当前线程名,验证是否异步
        System.out.println("发送邮件线程: " + Thread.currentThread().getName());
    }
}

验证方式很直接:异步生效时线程名是 task-1 之类的线程池线程;不生效时会是 http-nio-8080-exec-1(还在 Tomcat 请求线程)。排查 @Async 第一件事永远是打印线程名,别靠猜。

坑一:同类自调用,@Async 直接失效(最高频)

这是踩得最多的坑。@Async 靠 Spring 生成的代理对象 拦截方法调用来实现异步。如果你在同一个类内部 ,用 this.xxx() 调另一个 @Async 方法,这次调用根本没走代理,直接是普通方法调用,异步就废了。

java 复制代码
@Service
public class OrderService {

    public void createOrder() {
        // ...下单逻辑...
        this.sendNotify(); // 自调用!没走代理,@Async 失效,同步执行
    }

    @Async
    public void sendNotify() {
        System.out.println("通知线程: " + Thread.currentThread().getName());
    }
}

createOrderthis.sendNotify() 打印出来的线程名还是请求线程,因为 this 是原始对象不是代理对象。

修法一:把异步方法拆到另一个 bean 里,通过注入调用(推荐,职责也更清晰)。

java 复制代码
@Service
public class OrderService {
    @Autowired
    private NotifyService notifyService; // 注入的是代理对象

    public void createOrder() {
        notifyService.sendNotify(); // 走代理,@Async 生效
    }
}

@Service
public class NotifyService {
    @Async
    public void sendNotify() {
        System.out.println("通知线程: " + Thread.currentThread().getName());
    }
}

修法二:实在不想拆类,注入自己(拿到代理对象再调)。

java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderService self; // 注入自身的代理

    public void createOrder() {
        self.sendNotify(); // 通过代理调用,生效
    }

    @Async
    public void sendNotify() { /* ... */ }
}

自调用失效不是 @Async 独有的,@Transactional@Cacheable 这些基于 AOP 代理的注解都一样。记住:代理只能拦截「从外部进来」的调用

坑二:private / final 方法上加 @Async 无效

因为 Spring 默认用 CGLIB 动态代理(继承目标类生成子类),代理靠重写方法 来织入逻辑。private 方法子类看不到、final 方法不能重写,所以这两种方法上的 @Async 都不会生效。

java 复制代码
@Service
public class ReportService {

    @Async
    private void gen() { }   // private:代理拦不到,无效

    @Async
    public final void export() { }  // final:不能被重写,无效
}

修法很简单:异步方法必须是 public 且非 final。这条同样适用于所有 AOP 注解。

坑三:默认线程池是「来一个开一个」,高并发会打爆

@Async 不指定线程池时,Spring Boot 默认用 SimpleAsyncTaskExecutor------它根本不是池,每次调用都新建一个线程,用完就扔,不复用、不限流。低频任务没事,一旦异步方法被高频调用,线程数会无限膨胀,直接把机器打到 OOM 或线程数耗尽。

一定要自己配一个有界的线程池:

java 复制代码
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("mailExecutor")
    public Executor mailExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);          // 核心线程数
        executor.setMaxPoolSize(8);           // 最大线程数
        executor.setQueueCapacity(100);       // 队列容量,满了才扩到 max
        executor.setThreadNamePrefix("mail-"); // 线程名前缀,方便日志排查
        // 队列满且线程满时,让调用方线程自己执行,起到背压保护作用
        executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

然后在 @Async按 bean 名字指定用哪个池:

java 复制代码
@Async("mailExecutor")
public void sendMail(String to) { /* ... */ }

不指定名字时用默认池,多个业务共用一个池容易互相拖累,建议按业务拆池。这里选 CallerRunsPolicy 是关键:队列满时不抛弃任务、也不无限扩线程,而是让提交任务的线程自己跑,自然形成背压,把上游压慢而不是压垮。

坑四:异步方法抛异常,你的日志里啥都没有

同步方法抛异常会往上冒,最终被全局异常处理器捕获。但 @Async 方法在别的线程跑,异常冒不回调用方,如果你没处理,它就被默默吞掉了------线上出问题连日志都找不到。

分两种情况。返回值是 void 的异步方法,配一个全局的 AsyncUncaughtExceptionHandler 兜住:

java 复制代码
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, params) -> {
            // 异步 void 方法抛的异常统一在这里打日志/告警
            System.err.println("异步方法 " + method.getName() + " 异常: "
                + throwable.getMessage());
        };
    }
}

返回值是 CompletableFuture 的异步方法,异常会被封装进 Future,在 get().exceptionally() 时才暴露:

java 复制代码
@Async("mailExecutor")
public CompletableFuture<String> fetch() {
    if (true) throw new RuntimeException("boom");
    return CompletableFuture.completedFuture("ok");
}

// 调用方
fetch().exceptionally(ex -> {
    System.err.println("捕获到异步异常: " + ex.getMessage());
    return "fallback";
});

结论:异步方法尽量返回 CompletableFuture 而不是 void ,这样调用方能拿到结果、也能处理异常;真要用 void,务必配 AsyncUncaughtExceptionHandler,否则异常人间蒸发。

小结

@Async 的坑几乎都源于「它是 AOP 代理实现的」这一个事实,记住这条主线就能自己推导:

  • 前提 :主类加 @EnableAsync + 方法加 @Async,缺一不异步;排查第一步永远是打印线程名确认。
  • 自调用失效 :同类内 this.xxx() 不走代理 → 拆到别的 bean 或注入自身代理。
  • private/final 无效:代理靠重写方法织入 → 异步方法必须 public 且非 final。
  • 默认线程池是隐患 :SimpleAsyncTaskExecutor 无限建线程 → 自配 ThreadPoolTaskExecutor 并用 @Async("名字") 指定,配 CallerRunsPolicy 做背压。
  • 异常会消失 :void 方法配 AsyncUncaughtExceptionHandler,或改返回 CompletableFuture.exceptionally() 处理。

一句话记忆点:@Async 不生效先看线程名,九成是自调用------凡是 AOP 代理的注解,都只拦得住「从外面进来」的调用。

相关推荐
wechatbot8881 小时前
解决企业微信官方 API 短板:iPad 协议全功能对接方案
java·汇编·windows·http·微信·企业微信
忘路之远近i1 小时前
受够阿里云自带终端后,我用 Cursor + grill-me 做了个运维面板
服务器·开发语言·人工智能·python·阿里云·云计算
Tim_101 小时前
【C++】020、野指针&悬空指针
java·开发语言
Flynt2 小时前
Spring Boot 3开虚拟线程,我把生产服务搞崩了两次
java·spring boot
郝学胜_神的一滴2 小时前
Python 高级编程 026:序列内核深剖
python·pycharm
SimonKing2 小时前
别再盲目跑测试了,用 JaCoCo 告诉你哪些代码根本没被覆盖
java·后端·程序员
唐青枫2 小时前
Java Grails 实战详解:用 Groovy 和 GORM 快速开发 Web 应用
java·groovy
SemiTris2 小时前
Java 异常体系深度解析:从误区到精通
java
Dxy12393102162 小时前
Python 实现POST上行压缩上传可用HTTP库汇总
开发语言·python·http