技术分享 2026-03-30 3 浏览

为什么大厂越来越少用 @Async 异步注解?坑比好处多

张经理
华创云枢

不知道大家有没有这种感觉:以前咱们写异步代码,第一反应就是加个 @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. 1. 线程池必须自定义,别用默认的 SimpleAsyncTaskExecutor
  2. 2. 异常必须捕获,做好补偿或重试机制
  3. 3. 事务问题,如果异步任务需要事务,考虑使用 TransactionTemplate 手动开启
  4. 4. 超时控制,使用 CompletableFuture 的 orTimeout() 设置超时
  5. 5. 幂等性,异步任务可能会重复执行,必须保证幂等

核心观点回顾

坑点
问题
解决方案
同类调用失效
内部调用不经过代理
注入自身或拆分 Service
异常丢失
异步异常无法传播
使用 CompletableFuture 回调
事务失效
新线程无事务上下文
手动使用 TransactionTemplate
线程池耗尽
默认无限制创建线程
自定义线程池,配置参数
返回值难处理
无法获取异步结果
CompletableFuture / MQ
内存泄漏
线程持有对象引用
使用 @PreDestroy 关闭


务实建议

能用同步解决的业务,不要强行异步。
必须异步的场景,优先用消息队列。
只用 @Async 的场景,务必自定义线程池。


相关文章