Spring框架中@Async注解与executor的核心差异是什么
很多人刚用Spring做异步开发的时候会把@Async和Executor当成两套独立的实现,其实二者根本不是同一抽象层级的东西,不存在谁完全替代谁的说法,核心差异和选型逻辑可以直接看下面的总结:
- 抽象定位不同
@Async是Spring基于AOP实现的声明式异步封装,底层执行任务还是靠Executor。它帮你屏蔽了线程池提交、返回值封装的样板逻辑,只要在方法上加注解就能把同步方法改成异步执行。
直接使用Executor是编程式的底层API调用,从线程池选型、任务提交、返回值处理到异常捕获全链路都需要你自己写代码实现,没有任何封装,灵活度最高,但重复代码多。
两种写法的代码差异可以直接看示例:
// @Async 写法,加注解即可,不需要手动提交任务 @Async("bizTaskExecutor") public CompletableFuture<Order> queryOrder(Long orderId) { return CompletableFuture.completedFuture(orderMapper.selectById(orderId)); }
// 直接使用Executor写法,需要手动注入线程池、提交任务 @Resource private ThreadPoolTaskExecutor bizTaskExecutor; public CompletableFuture<Order> queryOrder(Long orderId) { return bizTaskExecutor.submit(() -> orderMapper.selectById(orderId)); }
这里要提一句:@Async默认用的SimpleAsyncTaskExecutor是不做线程复用的,来一个任务新建一个线程,高并发场景下直接把内存打满,这也是很多人踩过的生产坑,用@Async必须自定义线程池,不要用默认实现。
生效限制不同
@Async因为是AOP代理实现,有几个天然的硬限制:同类内部调用@Async方法不生效、非public方法不生效、方法不能是final的。如果要透传请求上下文(链路ID、登录态、MDC日志标记),还需要额外实现TaskDecorator做上下文拷贝,配置不对就会出现上下文丢失的问题。
手动用Executor没有这些限制,你可以在任何代码位置提交任务,上下文拷贝直接在提交任务的时候手动完成就行,不需要额外做AOP适配,逻辑完全可控。异常处理逻辑不同
@Async的默认异常处理逻辑很容易踩坑:如果异步方法是void返回值,抛出的异常会被全局AsyncUncaughtExceptionHandler拦截,你不自定义处理器的话,异常只会打一行日志,很容易被忽略;如果返回值是Future类,异常会被封装到返回对象里,只有调用get()方法的时候才会感知到。
手动用Executor提交任务的时候,你可以直接在任务逻辑里包try-catch,异常怎么打日志、怎么触发告警、怎么做降级重试全是显式写在代码里的,不会出现异常被悄咪咪吞掉的情况。
不用纠结谁更好,根据场景选就行:
- 选@Async的场景
- 非核心链路的简单异步逻辑,比如发站内信、同步非关键数据、打操作日志,不需要复杂的任务管控
- 团队已经做好了@Async的全局标准化配置:定义好了参数合理的业务线程池、配好了上下文透传的装饰器、实现了全局异步异常告警、明确了AOP失效的规避规则,这种场景下用@Async能省很多重复代码,开发效率很高
- 异步逻辑没有复杂的编排需求,就是单纯的方法异步执行,返回标准CompletableFuture
- 选手动调用Executor的场景
- 核心异步链路,需要精细管控任务生命周期:比如要做任务级别的监控(排队时长、执行时长、拒绝率统计)、支持动态调整线程池参数、需要对特定任务做取消/重试操作
- 上下文传递逻辑复杂:比如需要透传多维度的请求标记、灰度参数、用户权限信息,手动提交任务时直接做上下文拷贝比给@Async写扩展更简单可靠
- 不想为了规避@Async的AOP限制特意拆分Service类,比如同类里的方法需要调用异步逻辑,手动提交任务不需要走代理,不会出现注解失效的问题
- 需要做复杂的任务调度:比如根据任务类型路由到不同参数的线程池(IO密集型任务走大队列大线程数的池、CPU密集型任务走小核心数的池)、实现优先级任务调度、自定义拒绝策略(比如拒绝后降级走同步执行、触发熔断)
- 对异常敏感度极高的场景,需要在任务提交、执行全链路做异常捕获和兜底处理,避免异常丢失
不存在“生产环境不能用@Async”的说法,绝大多数@Async的线上问题都是使用者没做自定义配置,直接用了默认的非线程池实现,或者没搞懂AOP生效规则导致注解失效,不是注解本身的设计缺陷。
@Async本身支持指定自定义Executor,你完全可以根据业务定义多个不同参数的线程池,给不同的异步方法分配对应的线程资源,本质上是在Executor的基础上做了一层声明式封装,二者不是互斥关系。
内容的提问来源于stack exchange,提问作者Hello Mr Mustafo

