当前位置: 首页 > news >正文

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

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

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

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

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

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

@SpringBootApplication@EnableAsync// 少了这个,@Async 完全被无视publicclassApp{publicstaticvoidmain(String[]args){SpringApplication.run(App.class,args);}}
@ServicepublicclassMailService{@AsyncpublicvoidsendMail(Stringto){// 打印当前线程名,验证是否异步System.out.println("发送邮件线程: "+Thread.currentThread().getName());}}

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

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

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

@ServicepublicclassOrderService{publicvoidcreateOrder(){// ...下单逻辑...this.sendNotify();// 自调用!没走代理,@Async 失效,同步执行}@AsyncpublicvoidsendNotify(){System.out.println("通知线程: "+Thread.currentThread().getName());}}

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

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

@ServicepublicclassOrderService{@AutowiredprivateNotifyServicenotifyService;// 注入的是代理对象publicvoidcreateOrder(){notifyService.sendNotify();// 走代理,@Async 生效}}@ServicepublicclassNotifyService{@AsyncpublicvoidsendNotify(){System.out.println("通知线程: "+Thread.currentThread().getName());}}

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

@ServicepublicclassOrderService{@AutowiredprivateOrderServiceself;// 注入自身的代理publicvoidcreateOrder(){self.sendNotify();// 通过代理调用,生效}@AsyncpublicvoidsendNotify(){/* ... */}}

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

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

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

@ServicepublicclassReportService{@Asyncprivatevoidgen(){}// private:代理拦不到,无效@Asyncpublicfinalvoidexport(){}// final:不能被重写,无效}

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

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

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

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

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

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

@Async("mailExecutor")publicvoidsendMail(Stringto){/* ... */}

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

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

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

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

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

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

@Async("mailExecutor")publicCompletableFuture<String>fetch(){if(true)thrownewRuntimeException("boom");returnCompletableFuture.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 代理的注解,都只拦得住「从外面进来」的调用。

http://www.jsqmd.com/news/1281076/

相关文章:

  • 2026 年浔阳区马桶疏通 菜盆疏通 抽粪,九江本地管道维修真实体验 - LYL仔仔
  • 如何用Marketch插件实现Sketch设计稿到前端代码的无缝转换
  • OpenClaw智能体框架四大安全漏洞剖析与企业级加固指南
  • Magpie:Windows平台终极窗口超分辨率工具完整指南
  • Langchain中间件-LLM工具模拟器设计与实践
  • 时间序列反事实必要性分析:从特征相关到因果推断的实践指南
  • C++多维数组:从内存布局到现代容器与性能优化实践
  • 物联网设备初级电池寿命优化方案与STM32低功耗设计
  • [WesternCTF2018]shrine-学习笔记
  • 大跨度柔性电动挡烟垂壁 消防3C认证防火防烟分区隔断
  • Hadoop+Spark+Hive构建小红书评论情感分析系统
  • RAG技术优化实战:7个提升大模型问答效果的核心技巧
  • Nintendo Switch大气层系统1.7.1:深度技术解析与高级定制指南
  • 惠州工贸公司代理记账推荐,5 家可做精细化经营账机构 - GrowUME
  • 思源宋体TTF版本:7款免费开源字体如何彻底解决中文排版难题
  • Sunshine游戏串流服务器:5步搭建你的家庭云游戏终极解决方案
  • 珠排序算法:从物理模型到C++实现的非比较排序解析
  • JAVA毕设项目: 基于 SpringBoot+Vue 的高校闲置资源数字化交易管理系统 校园二手交易诚信评价与订单管理系统(源码+文档,讲解、调试运行,定制等)
  • QQ空间历史说说完整导出终极指南:三步找回青春记忆的免费工具
  • RFID模块TOY0019实战:从硬件连接到Arduino集成的完整调试指南
  • 企业级大模型安全实战:从六大攻击类型到纵深防御体系构建
  • 2026年四川优质认证实力企业推荐:四川企诚星科技咨询有限公司 - 深度智识库
  • VisualCppRedist AIO:3步彻底解决Windows软件运行库缺失问题
  • 终极缠论量化分析插件:让通达信自动识别买卖信号
  • 从cdn说起
  • 工科实践利器:拓竹A1C 3D打印从建模到后处理全流程指南
  • html中实现拨打电话发短信功能 js实现拨打电话发短信功能
  • BilibiliDown:小白也能轻松下载B站视频的完整指南
  • HTML学习笔记——HTML基本标记
  • SinaL2:用Python轻松获取新浪Level2行情数据的实战指南