Spring Boot 多线程的 6 种用法,我都帮你踩过坑了
目录
做后端开发久了,有一类问题绕不开:用户下单,要同时发短信、扣库存、推消息;跑报表,一跑就几十秒,把主线程堵死;定时任务,上一个还没跑完,下一个又启动了……
这些问题本质上都是"怎么把任务交给别的线程去跑"。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());
// 统计数据、清理缓存、同步第三方……
}
}
fixedRate 和 fixedDelay 的区别顺带说一下:fixedRate 是从任务开始算间隔,fixedDelay 是从任务结束算间隔。任务本身耗时较长时,两种配置会有明显差异。
适合的场景: 定时报表、定时清理过期数据、定时同步第三方系统。
对比总结
| 方式 | 适合场景 | 能拿到返回值 | 复杂度 |
|---|---|---|---|
| @Async | 简单异步,不关心结果 | 支持(Future) | 低 |
| 显式线程池 | 高并发、精细控制 | 支持 | 中 |
| CompletableFuture | 多任务编排、有依赖关系 | 支持 | 中 |
| 事件监听 + @Async | 主流程解耦、多下游触发 | 不关心 | 中 |
| JDK 原生线程池 | 非 Spring 环境、临时任务 | 支持 | 低 |
| 定时 + @Async | 周期性后台任务 | 不关心 | 低 |
最后说一句
选型上没有银弹,但有一个实用原则:
- 生产环境优先选择**@Async + 自定义线程池**的组合方案:既保证开发效率,又能通过线程池参数控制避免资源耗尽;
- 复杂业务流(如下单、支付)建议使用CompletableFuture做任务编排;
不管用哪种,有一件事不能省:一定要配自定义线程池,一定要设合理的队列上限和拒绝策略。线程池用得不好,出了问题比不用多线程还难排查。
详细请阅读我之前写的文章:
更多推荐



所有评论(0)