目录

一、@Async 注解:大多数情况下的首选

二、显式使用线程池:需要精细控制时

三、CompletableFuture:多个任务要编排时

四、事件监听 + @Async:解耦的优雅方式

五、JDK 原生线程池:不想引入 Spring 依赖时

六、@Scheduled + @Async:定时任务不阻塞

对比总结

最后说一句

详细请阅读我之前写的文章:


做后端开发久了,有一类问题绕不开:用户下单,要同时发短信、扣库存、推消息;跑报表,一跑就几十秒,把主线程堵死;定时任务,上一个还没跑完,下一个又启动了……

这些问题本质上都是"怎么把任务交给别的线程去跑"。Spring Boot 提供了不止一种方式,下面把我实际用过的 6 种整理出来,不同场景对号入座。


一、@Async 注解:大多数情况下的首选

这是开发中用得最多的方式,核心原理是 AOP 动态代理——加了 @Async 的方法,Spring 会帮你把它丢到线程池里跑,调用方不需要等。

第一步,配置线程池并开启异步支持:

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);        // 核心线程数,建议和 CPU 核心数持平
        executor.setMaxPoolSize(8);         // 最大线程数
        executor.setQueueCapacity(100);     // 任务队列,缓冲突发请求
        executor.setThreadNamePrefix("Async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

有一点要特别说:@EnableAsync 如果不配自定义线程池,Spring 默认用 SimpleAsyncTaskExecutor,这玩意每次都新建线程,没有复用,高并发场景会直接把线程打爆。生产环境务必配自定义线程池。

第二步,在需要异步的方法上加注解:

@Service
public class UserService {

    @Async("taskExecutor")
    public void sendEmail(String email) {
        System.out.println(Thread.currentThread().getName() + " 发送邮件至:" + email);
    }
}

就这两步,调用 sendEmail 的主线程立刻返回,邮件在后台线程里发。

适合的场景: 发邮件、发短信、写操作日志、推送通知——这些任务不需要立即拿到结果,只管丢过去就行。


二、显式使用线程池:需要精细控制时

@Async 够简单,但有时候你需要在代码里动态决定要不要异步、提交多少任务、什么时候等结果。这时候直接注入线程池,手动提交任务更灵活。

@Service
public class ReportService {

    @Resource(name = "taskExecutor")
    private ThreadPoolTaskExecutor executor;

    public void generateReport() {
        executor.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " 生成报表中...");
            // 查询数据、计算、导出
        });
    }
}

线程池的几个核心参数值得说清楚:

  • corePoolSize:常驻线程数,任务少的时候维持这些线程待命
  • maxPoolSize:流量突增时最多能开多少线程
  • queueCapacity:队列满了才会扩线程到 max,不是一来任务就扩
  • keepAliveSeconds:超出 core 的线程,空闲这么久就销毁

理解这个顺序很重要:来了任务先给核心线程 → 核心线程满了进队列 → 队列满了扩线程到 max → max 也满了触发拒绝策略。很多人以为队列满了才扩线程,但实际上只有核心线程都在跑才进队列。

适合的场景: 批量数据导入、大文件解析、高并发计算,需要精准控制线程行为的场景。


三、CompletableFuture:多个任务要编排时

前两种方式有个共同的短板:任务一旦提交,主线程就不管了。但实际业务里经常需要"等这几个任务都跑完,再做下一步",比如下单前要同时校验库存和余额,两个都通过才能创建订单。

CompletableFuture 就是为这类场景而生的。

@Service
public class OrderService {

    public CompletableFuture<Boolean> checkInventory() {
        return CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " 检查库存");
            return true;
        });
    }

    public CompletableFuture<Boolean> deductBalance() {
        return CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " 扣减余额");
            return true;
        });
    }

    public void placeOrder() {
        // 两个任务并行跑,全部完成后再创建订单
        CompletableFuture.allOf(checkInventory(), deductBalance())
                .thenRun(() -> {
                    System.out.println(Thread.currentThread().getName() + " 校验通过,创建订单");
                });
    }
}

几个常用的组合方法:

  • allOf:等所有任务都完成
  • anyOf:任意一个完成就继续
  • thenApply:上一个任务的结果作为输入,处理后传给下一步
  • thenCompose:把两个 Future 串联起来(第一个的结果触发第二个)
  • exceptionally:某个任务失败时的兜底处理

适合的场景: 下单流程中的多项并行校验、聚合多个接口的查询结果、有依赖关系的任务链。


四、事件监听 + @Async:解耦的优雅方式

订单创建完成后,要发通知、加积分、更新统计……这些后续操作如果全写在 createOrder 方法里,这个方法很快会变成一坨屎。

Spring 的事件机制可以解决这个问题:订单服务只管发布"订单创建"这个事件,其他模块各自监听、各自处理,互不干扰,加 @Async 就异步了。

第一步,定义事件:

public class OrderCreatedEvent extends ApplicationEvent {
    private final Long orderId;

    public OrderCreatedEvent(Object source, Long orderId) {
        super(source);
        this.orderId = orderId;
    }

    public Long getOrderId() {
        return orderId;
    }
}

第二步,发布事件:

@Service
public class OrderService {

    @Resource
    private ApplicationEventPublisher publisher;

    public void createOrder() {
        Long orderId = 123L;
        // 核心逻辑
        System.out.println("订单创建完成,ID:" + orderId);
        // 发布事件,后续由各监听器处理
        publisher.publishEvent(new OrderCreatedEvent(this, orderId));
    }
}

第三步,异步监听:

@Component
public class OrderListener {

    @Async("taskExecutor")
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        System.out.println(Thread.currentThread().getName() 
            + " 处理订单事件,ID:" + event.getOrderId());
        // 发通知、加积分、更新报表……各自在这里实现
    }
}

适合的场景: 主流程完成后需要触发多个下游动作,且这些动作相互独立、不影响主流程返回结果的场景。


五、JDK 原生线程池:不想引入 Spring 依赖时

有些工具类、独立模块不在 Spring 容器里,或者只是写个简单的并发任务,不需要那么重的封装,直接用 JDK 的 ExecutorService 就够了。

@Service
public class DataService {

    private final ExecutorService executor = Executors.newFixedThreadPool(10, r -> {
        Thread t = new Thread(r);
        t.setName("JdkAsync-" + t.getId());
        return t;
    });

    public void processData() {
        executor.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " 处理数据");
        });
    }
}

要注意的是,Executors.newFixedThreadPool 的队列是无界的 LinkedBlockingQueue,任务堆积时可能 OOM。线上项目最好手动 new ThreadPoolExecutor(...) 把各项参数显式写清楚,不要图方便用工厂方法。

适合的场景: 和 Spring 容器无关的并发场景、临时性的短生命周期任务。


六、@Scheduled + @Async:定时任务不阻塞

定时任务有个经典的坑:默认是单线程执行的,上一个任务没跑完,下一个就得等。如果某次任务卡住了,后续所有定时任务全部排队,系统就乱了。

加上 @Async 可以让每次定时触发都在独立线程里跑:

@Configuration
@EnableAsync
@EnableScheduling
public class ScheduledConfig {

    @Async("taskExecutor")
    @Scheduled(fixedRate = 5000)
    public void reportJob() {
        System.out.println(Thread.currentThread().getName() 
            + " 执行报表统计,时间:" + System.currentTimeMillis());
        // 统计数据、清理缓存、同步第三方……
    }
}

fixedRatefixedDelay 的区别顺带说一下:fixedRate 是从任务开始算间隔,fixedDelay 是从任务结束算间隔。任务本身耗时较长时,两种配置会有明显差异。

适合的场景: 定时报表、定时清理过期数据、定时同步第三方系统。


对比总结

方式 适合场景 能拿到返回值 复杂度
@Async 简单异步,不关心结果 支持(Future)
显式线程池 高并发、精细控制 支持
CompletableFuture 多任务编排、有依赖关系 支持
事件监听 + @Async 主流程解耦、多下游触发 不关心
JDK 原生线程池 非 Spring 环境、临时任务 支持
定时 + @Async 周期性后台任务 不关心

最后说一句

选型上没有银弹,但有一个实用原则:

  1. 生产环境优先选择**@Async + 自定义线程池**的组合方案:既保证开发效率,又能通过线程池参数控制避免资源耗尽;
  2. 复杂业务流(如下单、支付)建议使用CompletableFuture做任务编排;

不管用哪种,有一件事不能省:一定要配自定义线程池,一定要设合理的队列上限和拒绝策略。线程池用得不好,出了问题比不用多线程还难排查。

详细请阅读我之前写的文章:

@Async注解失效的 9 种场景

CompletableFuture使用详解

线程池项目中实际应用

线程池知多少?

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐