不知道大家有没有这种感觉:以前咱们写异步代码,第一反应就是加个 @Async 注解,简单又方便。但现在去大厂面试,或者看看他们的代码库,你会发现 @Async 越来越少了。
这不是错觉。阿里、美团、字节这些大厂的内部规约,都对 @Async 做了诸多限制,甚至直接禁用。
所以曾经风光无限的 @Async,为什么现在变成了"坑"的代名词?
什么是 @Async?
@Async 是 Spring 提供的异步执行注解。咱们只要在方法上加这个注解,Spring 就会把这个方法的执行丢到一个独立的线程池中去跑,不阻塞主线程。
@Service
public class OrderService {
@Async
public void sendNotification(Long orderId) {
// 发送短信/邮件
System.out.println("发送通知中...");
}
public void createOrder(Order order) {
// 创建订单的主流程
orderMapper.insert(order);
// 异步发送通知,不阻塞下单流程
sendNotification(order.getId());
}
}看起来很美好对不对?主线程不阻塞,响应时间咔咔下降。
为什么曾经大家都在用?
• ✅ 配置简单,加个注解就行 • ✅ 不需要手动管理线程 • ✅ 看起来解决了性能问题
但问题来了 —— 用起来爽,出问题的时候,那叫一个酸爽。
坑点1:同类调用 @Async 失效(最容易被忽略的坑)
问题描述
@Service
public class OrderService {
@Async
public void sendNotification(Long orderId) {
// 发送短信/邮件
System.out.println("当前线程: " + Thread.currentThread().getName());
}
public void createOrder(Order order) {
// 创建订单的主流程
orderMapper.insert(order);
// 坑!同类中直接调用,@Async 不会生效!
sendNotification(order.getId());
// 实际上还是在主线程同步执行,和普通方法调用没区别
}
}运行结果:
当前线程: http-nio-8080-exec-1 ← 仍然是主线程!
原因分析
@Async 的实现原理是 Spring AOP 代理:
调用流程:
createOrder() [主线程]
↓
Spring 代理对象 OrderService
↓
sendNotification() 被增强 → 丢到线程池执行但同类内部调用时:
createOrder() [主线程]
↓
this.sendNotification() ← 直接调用原始对象
↓
没有经过代理,@Async 增强不生效!就像你请了个秘书帮你接电话(代理),但你自己直接打电话(内部调用),秘书根本不知道这事儿。
代码对比
❌ 错误写法(同类调用):
@Service
public class OrderService {
@Async
public void sendNotification(Long orderId) {
// 不会异步执行!
}
public void createOrder(Order order) {
sendNotification(order.getId()); // 直接调用,无效
}
}✅ 正确写法(注入自身或注入其他Service):
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自身
@Async
public void sendNotification(Long orderId) {
// 现在会异步执行了!
}
public void createOrder(Order order) {
orderMapper.insert(order);
self.sendNotification(order.getId()); // 通过代理对象调用
}
}或者更推荐的做法(拆分成两个Service):
@Service
public class OrderService {
@Autowired
private NotificationService notificationService;
public void createOrder(Order order) {
orderMapper.insert(order);
notificationService.sendNotification(order.getId()); // 不同Service,通过代理调用
}
}
@Service
public class NotificationService {
@Async
public void sendNotification(Long orderId) {
// 正常异步执行
}
}关键要点总结
同类中 @Async 方法不会生效,必须通过代理对象调用(注入自身或拆分到其他 Service)。
坑点2:异常丢失,排查困难
问题描述
@Async 方法抛出的异常,不会传播到调用方。调用方以为成功了,实际上可能已经炸了。
@Service
public class PaymentService {
@Async
public void processPayment(Long orderId) {
// 扣款逻辑
if (insufficientBalance) {
throw new RuntimeException("余额不足");
}
// 这里抛异常,调用方完全感知不到!
}
public void checkout(Long orderId) {
// 业务逻辑
processPayment(orderId); // 调用后直接返回,以为支付成功了
// 实际上可能已经失败了,但用户看到的是"成功"
}
}原因分析
@Async 的本质是把方法调用封装成 Runnable,丢到线程池去执行。就像你把一封信丢进邮筒,邮局有没有送到,你根本不知道。
关键要点总结
@Async 方法的异常不会抛到调用方,必须做好兜底方案。
坑点3:事务失效,同步变异步的坑
问题描述
这个坑可能 90% 的人都踩过。咱们看代码:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockService stockService;
@Async
public void createOrder(Order order) {
// 扣库存
stockService.deductStock(order.getItems());
// 创建订单
orderMapper.insert(order);
}
}问题: 如果 deductStock 成功了,但 orderMapper.insert 失败了 —— 库存扣了,订单没创建。数据不一致!
原因分析
@Async 会开启新的线程,而 Spring 的事务是基于线程本地变量(ThreadLocal) 实现的。新线程里,根本拿不到主线程的事务上下文!
// 伪代码解释原理
public class TransactionSynchronizationManager {
// 这个 ThreadLocal 里存着当前线程的事务连接
private static final ThreadLocal<TransactionContext> resources = new ThreadLocal<>();
}主线程开启事务 → 丢给 @Async 子线程 → 子线程的 ThreadLocal 是空的 → 事务失效
关键要点总结
@Async 方法默认不在主线程的事务上下文中,异步方法内部无法参与主线程的事务。
坑点4:线程池耗尽,系统雪崩
问题描述
Spring 默认的 @Async 线程池是 SimpleAsyncTaskExecutor —— 这货没有线程数限制!每来一个任务就创建一个新线程。
// Spring 默认配置(有问题!)
@Async
public void handleRequest(List<Long> ids) {
for (Long id : ids) {
// 每个 id 都会创建新线程!!!
processItem(id);
}
}如果有 10 万条数据,直接给你创建 10 万个线程,系统直接挂掉。
实际案例
// 某个活动接口,查询 10 万用户发送通知
@GetMapping("/send-notifications")
public void sendNotifications() {
List<User> users = userMapper.selectAll();
users.forEach(user -> {
notificationService.sendAsync(user.getId()); // 每个用户一个线程
});
}结果: 请求刚发出去,服务器 CPU 100%,线程池爆炸,所有接口响应超时。
关键要点总结
务必自定义线程池,配置核心线程数、最大线程数、队列大小、拒绝策略。
坑点5:返回值难处理
问题描述
如果需要异步方法的返回值,那叫一个麻烦。
@Async
public Future<String> processData(Long id) {
// 处理业务
return new AsyncResult<>("处理完成");
}
public void main() {
// 调用方
Future<String> future = processData(1L);
// 如果不调用 get() 阻塞等待,根本拿不到结果
// 那和同步有什么区别?
String result = future.get(); // 阻塞!
}代码对比
优化前(有问题):
@Async
public void asyncProcess(Long id) {
// 不知道结果,无法回调
}优化后(推荐):
// 方案一:使用 CompletableFuture
public CompletableFuture<Result> asyncProcess(Long id) {
return CompletableFuture.supplyAsync(() -> {
// 业务逻辑
return Result.success();
}, customExecutor);
}
// 方案二:使用消息队列
public void asyncProcess(Long id) {
// 丢到 MQ,由消费者处理
mqTemplate.send("process-queue", id);
}关键要点总结
如果需要异步结果的回调,优先考虑 CompletableFuture 或消息队列。
坑点6:内存泄漏风险
问题描述
@Async 使用的线程池,如果没正确关闭,会导致内存泄漏。
@Service
public class MyService {
@Async
public void doWork() {
// 线程池中的线程会持有当前类的引用
// 如果 Service 被销毁但线程池没关闭 -> 内存泄漏
}
}原因分析
线程池中的线程是长生命周期的,会持有业务对象的引用。如果业务对象销毁了但线程还在跑,GC 无法回收。
关键要点总结
确保线程池在应用关闭时正确销毁,使用
@PreDestroy标记清理方法。
替代方案一:手动线程池(推荐)
@Configuration
public class AsyncConfig {
private static final int CORE_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 50;
private static final int QUEUE_CAPACITY = 200;
private static final long KEEP_ALIVE_SECONDS = 60;
@Bean("customAsyncExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(CORE_POOL_SIZE);
executor.setMaxPoolSize(MAX_POOL_SIZE);
executor.setQueueCapacity(QUEUE_CAPACITY);
executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
executor.setThreadNamePrefix("custom-async-");
// 拒绝策略:由调用线程执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}使用:
@Service
public class NotificationService {
@Async("customAsyncExecutor")
public void sendSms(String phone, String content) {
// 发送短信
}
}替代方案二:CompletableFuture(推荐)
@Service
public class OrderService {
@Autowired
private Executor customExecutor;
public void createOrder(Order order) {
// 主业务逻辑
orderMapper.insert(order);
// 异步执行,并获取结果
CompletableFuture<Void> stockFuture = CompletableFuture.runAsync(
() -> stockService.deductStock(order.getItems()),
customExecutor
);
CompletableFuture<Void> notificationFuture = CompletableFuture.runAsync(
() -> notificationService.sendNotification(order.getId()),
customExecutor
);
// 等待所有异步任务完成
CompletableFuture.allOf(stockFuture, notificationFuture).join();
}
}替代方案三:消息队列(终极方案)
@Service
public class OrderService {
@Autowired
private RocketMQTemplate mqTemplate;
public void createOrder(Order order) {
// 1. 主业务同步执行
orderMapper.insert(order);
// 2. 发送延迟消息
mqTemplate.asyncSend("order-notify-topic", order.getId(), new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("消息发送成功");
}
@Override
public void onException(Throwable e) {
log.error("消息发送失败", e);
// 失败补偿
}
});
}
}
// 消费者
@Component
@RocketMQMessageListener(topic = "order-notify-topic", consumerGroup = "order-consumer")
public class OrderNotifyConsumer implements RocketMQListener<Long> {
@Override
public void onMessage(Long orderId) {
// 发送通知
notificationService.sendNotification(orderId);
}
}注意事项提醒
1. 线程池必须自定义,别用默认的 SimpleAsyncTaskExecutor2. 异常必须捕获,做好补偿或重试机制 3. 事务问题,如果异步任务需要事务,考虑使用 TransactionTemplate手动开启4. 超时控制,使用 CompletableFuture的orTimeout()设置超时5. 幂等性,异步任务可能会重复执行,必须保证幂等
核心观点回顾
@PreDestroy 关闭 |
务实建议
能用同步解决的业务,不要强行异步。
必须异步的场景,优先用消息队列。
只用 @Async 的场景,务必自定义线程池。