@Async异步方法的6个常见坑:代理失效、线程池配置、异常吞咽
@Async异步方法的6个常见坑:代理失效、线程池配置、异常吞咽
适读人群:Spring Boot开发者,有@Async使用经验并想深入理解其原理的工程师 | 阅读时长:约17分钟
开篇故事
有一次线上告警,某个发送通知的异步任务报了NPE,但日志里没有任何异常记录,监控显示任务一直没执行完。
翻了半天,最终发现是两个问题叠加:第一,@Async方法内部抛了异常,但因为返回值是void,异常被AsyncUncaughtExceptionHandler默认实现静默吞掉了;第二,自定义的线程池配置了CallerRunsPolicy,但被遗漏了日志配置,异常发生时没有输出到任何地方。
更早的时候我还踩过"明明加了@Async,方法还是同步执行"的坑——是因为在同一个类里调用了自己的@Async方法,代理失效了。
@Async看起来简单,但坑确实多。今天把我踩过的6个坑全部整理出来,每个都结合源码解释根本原因。
一、@Async的工作原理
@Async的实现依赖AOP代理。当你调用一个带@Async注解的方法时,实际上调用的是代理对象的方法,代理把方法执行包装成一个Callable/Runnable,提交到线程池里异步执行。
核心类:
AsyncAnnotationBeanPostProcessor:为有@Async方法的Bean创建代理AsyncExecutionInterceptor:方法拦截器,负责把方法提交到线程池AsyncTaskExecutor:线程池接口
二、源码核心路径解析
2.1 @Async执行流程
2.2 AsyncExecutionInterceptor.invoke源码
// AsyncExecutionInterceptor.java 第97行(简化)
@Override
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
Class<?> targetClass = (invocation.getThis() != null ?
AopUtils.getTargetClass(invocation.getThis()) : null);
Method specificMethod = ClassUtils.getMostSpecificMethod(
invocation.getMethod(), targetClass);
final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
// 1. 确定使用哪个Executor
AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
if (executor == null) {
throw new IllegalStateException("No executor specified and no default executor set on AsyncExecutionInterceptor either");
}
// 2. 把方法调用包装成Callable
Callable<Object> task = () -> {
try {
Object result = invocation.proceed();
if (result instanceof Future<?> future) {
return future.get();
}
} catch (ExecutionException ex) {
handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
} catch (Throwable ex) {
handleError(ex, userDeclaredMethod, invocation.getArguments());
}
return null;
};
// 3. 提交到线程池
return doSubmit(task, executor, invocation.getMethod().getReturnType());
}
protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor,
Class<?> returnType) {
if (CompletableFuture.class.isAssignableFrom(returnType)) {
return CompletableFuture.supplyAsync(() -> {
try { return task.call(); } catch (Exception ex) { throw new RuntimeException(ex); }
}, executor);
} else if (ListenableFuture.class.isAssignableFrom(returnType)) {
return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
} else if (Future.class.isAssignableFrom(returnType)) {
return executor.submit(task);
} else {
executor.submit(task); // void方法:提交但不返回Future
return null;
}
}三、6个常见坑及解决方案
坑1:同类调用导致代理失效(最常见)
现象:@Async方法是同步执行的,没有在新线程里运行。
根因:Spring AOP基于代理,同一个类内部方法调用绕过了代理:
@Service
public class OrderService {
public void createOrder(Order order) {
saveOrder(order);
sendNotification(order); // 直接调用,不经过代理!@Async无效
}
@Async
public void sendNotification(Order order) {
// 这里实际上是同步执行的
System.out.println("Thread: " + Thread.currentThread().getName());
// 输出的是调用线程,而不是异步线程
}
}解决方案:
// 方案1:把@Async方法移到另一个Bean
@Component
public class NotificationService {
@Async
public void sendNotification(Order order) { ... }
}@Service
public class OrderService {
@Autowired
private NotificationService notificationService;
public void createOrder(Order order) {
saveOrder(order);
notificationService.sendNotification(order); // 经过代理,@Async生效
}
}
// 方案2:通过ApplicationContext自注入(不推荐,代码丑)
@Service
public class OrderService implements ApplicationContextAware {
private OrderService self;
@Override
public void setApplicationContext(ApplicationContext ctx) {
this.self = ctx.getBean(OrderService.class); // 获取代理对象
}
public void createOrder(Order order) {
saveOrder(order);
self.sendNotification(order); // 通过代理调用
}
@Async
public void sendNotification(Order order) { ... }
}坑2:没有配置线程池,使用SimpleAsyncTaskExecutor
根因:如果没有定义任何TaskExecutor Bean,@Async默认使用SimpleAsyncTaskExecutor——它不是线程池,每次调用都创建一个新线程,高并发下会把系统线程耗尽。
// 错误:没有配置,使用默认的SimpleAsyncTaskExecutor
@EnableAsync
@SpringBootApplication
public class App { ... }
// 正确:必须显式配置线程池
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncExceptionHandler();
}
}坑3:异常被静默吞掉
根因:void返回值的@Async方法抛出的异常,会被SimpleAsyncUncaughtExceptionHandler处理,默认只打印日志,调用方感知不到。
// SimpleAsyncUncaughtExceptionHandler.java
public class SimpleAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
if (logger.isErrorEnabled()) {
logger.error("Unexpected exception occurred invoking async method: " + method, ex);
}
// 就这样,异常被消化了
}
}解决方案:实现自定义的AsyncUncaughtExceptionHandler:
@Component
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Autowired
private AlertService alertService;
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("Async method failed: {}.{}({})",
method.getDeclaringClass().getSimpleName(),
method.getName(),
Arrays.toString(params), ex);
// 关键告警任务失败,要打告警
if (method.isAnnotationPresent(CriticalAsync.class)) {
alertService.sendAlert("Critical async task failed: " + method.getName());
}
}
}如果@Async方法有返回值(Future<T>或CompletableFuture<T>),调用方需要主动处理异常:
@Async
public CompletableFuture<String> asyncTask() {
if (someCondition) {
throw new RuntimeException("Task failed");
}
return CompletableFuture.completedFuture("success");
}
// 调用方必须处理异常
CompletableFuture<String> future = asyncService.asyncTask();
future.exceptionally(ex -> {
log.error("Async task error", ex);
return "fallback";
});坑4:@Async方法上的@Transactional失效
现象:@Async+@Transactional同时使用,事务没有开启。
根因:@Async在新线程执行,@Transactional的事务上下文是线程绑定的(ThreadLocal存储),新线程没有原来的事务上下文,所以@Transactional需要在新线程里重新创建事务。
这其实是正常行为——异步方法运行在新线程,自然开启了新事务。但如果你期望它参与调用方的事务,那是做不到的。
如果确实需要:在@Async方法内部使用REQUIRES_NEW传播级别是合适的,它会创建独立事务。
坑5:线程池队列满了的处理策略
现象:高并发时大量任务被拒绝,报RejectedExecutionException。
根因:线程池队列容量设置不合理,或者RejectedExecutionHandler策略不当。
四种拒绝策略的影响:
AbortPolicy(默认):抛RejectedExecutionException,调用方感知到异常CallerRunsPolicy:调用方线程直接执行,不会丢失任务,但会阻塞调用方DiscardPolicy:静默丢弃,任务消失无踪DiscardOldestPolicy:丢弃最旧的任务,可能丢失重要任务
// 生产推荐:使用CallerRunsPolicy防止任务丢失,并监控队列深度
@Bean
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("biz-async-");
// 自定义拒绝策略:记录日志 + CallerRunsPolicy
executor.setRejectedExecutionHandler((runnable, pool) -> {
log.warn("Thread pool rejected task! ActiveCount={}, QueueSize={}",
pool.getActiveCount(), pool.getQueue().size());
// 降级:调用方直接执行(不丢任务)
if (!pool.isShutdown()) {
runnable.run();
}
});
executor.initialize();
return executor;
}坑6:多个线程池时@Async不知道用哪个
// 有多个Executor Bean时,通过value指定
@Async("emailExecutor") // 指定用emailExecutor
public void sendEmail(String to) { ... }
@Async("smsExecutor")
public void sendSms(String phone) { ... }
// 配置多个线程池
@Configuration
public class AsyncConfig {
@Bean("emailExecutor")
public ThreadPoolTaskExecutor emailExecutor() {
ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(5);
ex.setMaxPoolSize(20);
ex.setQueueCapacity(100);
ex.setThreadNamePrefix("email-");
ex.initialize();
return ex;
}
@Bean("smsExecutor")
public ThreadPoolTaskExecutor smsExecutor() {
ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(10);
ex.setMaxPoolSize(30);
ex.setQueueCapacity(200);
ex.setThreadNamePrefix("sms-");
ex.initialize();
return ex;
}
}四、完整生产级@Async配置
@Configuration
@EnableAsync
public class ProductionAsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return defaultAsyncExecutor();
}
@Bean("defaultAsyncExecutor")
public ThreadPoolTaskExecutor defaultAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// CPU密集型:corePoolSize = CPU核数
// IO密集型:corePoolSize = CPU核数 * (1 + 等待时间/计算时间)
int cpuCount = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(cpuCount * 2);
executor.setMaxPoolSize(cpuCount * 4);
executor.setQueueCapacity(cpuCount * 100);
executor.setKeepAliveSeconds(60);
executor.setAllowCoreThreadTimeOut(true);
executor.setThreadNamePrefix("default-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 优雅停机:等待所有任务完成再关闭
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Uncaught async exception in {}.{}: {}",
method.getDeclaringClass().getSimpleName(),
method.getName(), ex.getMessage(), ex);
// 接入告警系统
Metrics.counter("async.errors",
"class", method.getDeclaringClass().getSimpleName(),
"method", method.getName()).increment();
};
}
}五、总结与延伸
@Async的6个坑汇总:
| 坑 | 根因 | 解决方案 |
|---|---|---|
| 代理失效 | 同类调用 | 拆Bean或自注入代理 |
| 线程耗尽 | 用了SimpleAsyncTaskExecutor | 显式配置ThreadPoolTaskExecutor |
| 异常吞掉 | void方法的异常被默认Handler消化 | 自定义AsyncUncaughtExceptionHandler |
| 事务失效 | 事务绑定线程,新线程没有事务 | 理解异步=新事务,在异步方法内管理事务 |
| 任务丢失 | AbortPolicy丢弃 | 使用CallerRunsPolicy或监控队列 |
| 执行器冲突 | 多Executor不知道用哪个 | @Async("beanName")明确指定 |
下一篇聊@Scheduled定时任务的源码实现,看ScheduledAnnotationBeanPostProcessor是怎么工作的。
