Spring @Async 与自定义线程池实战指南
一、概述
@Async 是 Spring 提供的异步执行注解,可以将方法的执行从调用线程转移到独立的线程池中,实现非阻塞调用。常用于日志记录、消息通知、数据同步等不需要同步等待结果的场景。
但 @Async 如果不配合自定义线程池使用,会带来严重的生产隐患。本文详细介绍 @Async 的使用方式、为什么必须自定义线程池、线程池参数如何设计,以及常见陷阱。
二、示例场景
一个订单系统,支付成功后需要:
- 更新订单状态(同步,必须成功)
- 发送短信通知(异步,失败不影响主流程)
- 记录操作日志(异步,失败不影响主流程)
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
三、@Async 基本使用
3.1 启用异步支持
java
@Configuration
@EnableAsync
public class AsyncConfig {
// 启用 @Async 注解支持
}
3.2 标记异步方法
java
@Service
public class NotificationService {
@Async
public void sendSmsNotification(String phone, String content) {
// 这个方法会在独立线程中执行,不阻塞调用方
smsClient.send(phone, content);
}
@Async
public void saveOperationLog(String userId, String action) {
OperationLog log = new OperationLog();
log.setUserId(userId);
log.setAction(action);
log.setCreateTime(new Date());
operationLogRepository.save(log);
}
}
3.3 调用方
java
@Service
public class OrderPaymentService {
@Resource
private NotificationService notificationService;
@Transactional(rollbackFor = Exception.class)
public void processPayment(PaymentDto dto) {
// 同步:更新订单状态
Order order = orderRepository.findByOrderNo(dto.getOrderNo());
order.setStatus("PAID");
orderRepository.save(order);
// 异步:发送通知(不阻塞主流程)
notificationService.sendSmsNotification(dto.getPhone(), "支付成功");
// 异步:记录日志(不阻塞主流程)
notificationService.saveOperationLog(dto.getUserId(), "PAYMENT_SUCCESS");
}
}
3.4 带返回值的异步方法
java
@Service
public class ReportService {
@Async
public Future<ReportResult> generateReport(String reportId) {
// 耗时操作
ReportResult result = doHeavyComputation(reportId);
return new AsyncResult<>(result);
}
// Java 8+ 推荐使用 CompletableFuture
@Async
public CompletableFuture<ReportResult> generateReportV2(String reportId) {
ReportResult result = doHeavyComputation(reportId);
return CompletableFuture.completedFuture(result);
}
}
// 调用方获取结果
@Service
public class ReportController {
@Resource
private ReportService reportService;
public ReportResult getReport(String reportId) throws Exception {
CompletableFuture<ReportResult> future = reportService.generateReportV2(reportId);
// 阻塞等待结果(最多等 30 秒)
return future.get(30, TimeUnit.SECONDS);
}
}
四、为什么必须自定义线程池
4.1 默认线程池的问题
如果不指定线程池,Spring 默认使用 SimpleAsyncTaskExecutor:
java
// SimpleAsyncTaskExecutor 的行为:
// 1. 每次调用都创建一个新线程
// 2. 没有线程复用
// 3. 没有队列缓冲
// 4. 没有最大线程数限制
生产事故场景:
高并发下:
- 每秒 1000 个请求,每个请求触发 2 个 @Async 调用
- 每秒创建 2000 个新线程
- 线程数持续增长,无上限
- 最终 OOM 或系统线程数耗尽,整个服务崩溃
4.2 Spring Boot 2.1+ 的默认行为
Spring Boot 2.1+ 自动配置了 ThreadPoolTaskExecutor,但默认参数可能不适合你的场景:
properties
# Spring Boot 默认配置
spring.task.execution.pool.core-size=8
spring.task.execution.pool.max-size=Integer.MAX_VALUE # 无限制!
spring.task.execution.pool.queue-capacity=Integer.MAX_VALUE # 无限制!
spring.task.execution.pool.keep-alive=60s
问题 :max-size 和 queue-capacity 都是无限制,高并发下仍可能 OOM。
4.3 自定义线程池的必要性
| 问题 | 默认行为 | 自定义解决 |
|---|---|---|
| 线程数无限增长 | 可能 OOM | 设置 maxPoolSize 上限 |
| 队列无限堆积 | 内存持续增长 | 设置 queueCapacity 上限 |
| 任务被丢弃无感知 | 静默失败 | 配置拒绝策略 |
| 线程无法区分来源 | 排查困难 | 设置线程名前缀 |
| 不同业务互相影响 | 共用一个池 | 按业务隔离线程池 |
五、自定义线程池配置
5.1 基础配置
java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("logExecutor")
public Executor logExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2); // 核心线程数
executor.setMaxPoolSize(5); // 最大线程数
executor.setQueueCapacity(1000); // 队列容量
executor.setKeepAliveSeconds(60); // 空闲线程存活时间
executor.setThreadNamePrefix("log-async-"); // 线程名前缀
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Bean("smsExecutor")
public Executor smsExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(500);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("sms-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
5.2 指定使用哪个线程池
java
@Service
public class NotificationService {
// 使用日志专用线程池
@Async("logExecutor")
public void saveOperationLog(String userId, String action) { ... }
// 使用短信专用线程池
@Async("smsExecutor")
public void sendSmsNotification(String phone, String content) { ... }
}
六、线程池参数设计
6.1 核心参数说明
任务提交流程:
1. 线程数 < corePoolSize -> 创建新线程执行
2. 线程数 >= corePoolSize -> 放入队列
3. 队列满 + 线程数 < maxPoolSize -> 创建新线程执行
4. 队列满 + 线程数 >= maxPoolSize -> 执行拒绝策略
6.2 参数设计原则
| 场景 | corePoolSize | maxPoolSize | queueCapacity | 说明 |
|---|---|---|---|---|
| IO 密集型(日志、HTTP调用) | CPU核数 * 2 | CPU核数 * 4 | 500~2000 | IO 等待时间长,需要更多线程 |
| CPU 密集型(计算、加密) | CPU核数 | CPU核数 + 1 | 100~500 | 线程过多反而增加上下文切换 |
| 低频任务(定时报表) | 1~2 | 3~5 | 100 | 不需要太多资源 |
| 高频轻量任务(日志记录) | 2~4 | 5~10 | 1000~5000 | 队列大一些缓冲突发流量 |
6.3 获取 CPU 核数
java
int cpuCores = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(cpuCores * 2);
executor.setMaxPoolSize(cpuCores * 4);
七、拒绝策略
7.1 四种内置策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
| CallerRunsPolicy | 由调用线程执行任务 | 不丢弃任务,但会阻塞调用方 |
| AbortPolicy | 抛出 RejectedExecutionException | 默认策略,需要调用方处理异常 |
| DiscardPolicy | 静默丢弃任务 | 允许丢失的非关键任务 |
| DiscardOldestPolicy | 丢弃队列中最老的任务 | 只关心最新数据的场景 |
7.2 推荐选择
java
// 日志记录场景:CallerRunsPolicy
// 队列满时由调用线程执行,保证日志不丢失,但可能短暂阻塞主线程
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 通知场景:DiscardPolicy 或自定义
// 通知丢失可接受,不能阻塞主流程
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
7.3 自定义拒绝策略
java
// 记录被拒绝的任务到文件日志,便于后续补偿
executor.setRejectedExecutionHandler((runnable, poolExecutor) -> {
log.error("线程池已满,任务被拒绝: {}", runnable.toString());
// 可以写入文件、发送告警等
});
八、@Async 的常见陷阱
8.1 自调用不生效
java
@Service
public class OrderService {
// 不生效!同类内部调用不走代理
public void methodA() {
this.asyncMethod(); // 直接调用,不会异步执行
}
@Async
public void asyncMethod() { ... }
}
解决方案:与 @Transactional 相同,拆分到不同 Bean 或注入自身代理。
8.2 异常被吞
java
@Async
public void asyncMethod() {
// 如果这里抛异常,调用方完全感知不到
// 异常会被线程池吞掉,只在日志中可见
throw new RuntimeException("异步方法异常");
}
解决方案:
java
// 方案1:方法内 try-catch
@Async
public void asyncMethod() {
try {
// 业务逻辑
} catch (Exception e) {
log.error("异步任务执行失败", e);
}
}
// 方案2:全局异常处理器
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, params) -> {
log.error("异步方法 {} 执行异常: {}", method.getName(), throwable.getMessage());
};
}
}
8.3 事务不传播
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void process() {
orderRepository.save(order);
// 异步方法在新线程中执行,不在当前事务中!
// 如果主事务回滚,异步方法的数据库操作不会回滚
asyncService.saveLog(order.getId());
}
}
原因:事务绑定在 ThreadLocal 中,新线程没有事务上下文。
解决:异步方法如果需要事务,必须自己开启独立事务。
8.4 方法必须是 public
java
// 不生效!private 方法无法被代理
@Async
private void asyncMethod() { ... }
// 不生效!static 方法无法被代理
@Async
public static void asyncMethod() { ... }
8.5 返回值为 void 时无法感知失败
java
// 调用方无法知道异步方法是否成功
@Async
public void fireAndForget() {
// 如果失败,调用方完全不知道
}
// 如果需要感知结果,使用 Future/CompletableFuture
@Async
public CompletableFuture<Boolean> asyncWithResult() {
try {
doSomething();
return CompletableFuture.completedFuture(true);
} catch (Exception e) {
return CompletableFuture.completedFuture(false);
}
}
九、线程池监控
9.1 暴露线程池指标
java
@Component
public class ThreadPoolMonitor {
@Resource
@Qualifier("logExecutor")
private ThreadPoolTaskExecutor logExecutor;
// 定时打印线程池状态
@Scheduled(fixedRate = 60000)
public void monitor() {
ThreadPoolExecutor pool = logExecutor.getThreadPoolExecutor();
log.info("线程池状态 - 活跃线程:{}, 核心线程:{}, 最大线程:{}, 队列大小:{}, 已完成:{}",
pool.getActiveCount(),
pool.getCorePoolSize(),
pool.getMaximumPoolSize(),
pool.getQueue().size(),
pool.getCompletedTaskCount());
}
}
9.2 关键监控指标
| 指标 | 含义 | 告警阈值建议 |
|---|---|---|
| activeCount | 当前活跃线程数 | > maxPoolSize * 0.8 |
| queueSize | 队列中等待的任务数 | > queueCapacity * 0.8 |
| completedTaskCount | 已完成任务总数 | 用于计算吞吐量 |
| rejectedCount | 被拒绝的任务数 | > 0 需要告警 |
十、优雅关闭
10.1 问题
应用关闭时,线程池中可能还有未完成的任务。如果直接关闭,任务会丢失。
10.2 配置优雅关闭
java
@Bean("logExecutor")
public Executor logExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("log-async-");
// 优雅关闭配置
executor.setWaitForTasksToCompleteOnShutdown(true); // 等待任务完成
executor.setAwaitTerminationSeconds(60); // 最多等待 60 秒
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
十一、业务隔离
11.1 为什么要隔离
java
// 反例:所有异步任务共用一个线程池
// 如果日志写入大量堆积,会影响短信发送
@Async
public void saveLog() { ... }
@Async
public void sendSms() { ... }
11.2 按业务划分线程池
java
@Configuration
@EnableAsync
public class AsyncConfig {
// 日志线程池:低优先级,队列大
@Bean("logExecutor")
public Executor logExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(2000);
executor.setThreadNamePrefix("log-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
executor.initialize();
return executor;
}
// 通知线程池:高优先级,队列小
@Bean("notifyExecutor")
public Executor notifyExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("notify-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
// 计算线程池:CPU 密集型
@Bean("computeExecutor")
public Executor computeExecutor() {
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(cpuCores);
executor.setMaxPoolSize(cpuCores + 1);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("compute-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
十二、方案对比总结
| 维度 | 默认线程池 | 自定义单线程池 | 按业务隔离线程池 |
|---|---|---|---|
| 实现复杂度 | 零配置 | 低 | 中 |
| 资源可控性 | 无 | 有 | 精细 |
| 业务隔离 | 无 | 无 | 有 |
| 故障影响范围 | 全局 | 全局 | 局部 |
| 监控粒度 | 无 | 整体 | 按业务 |
| 推荐程度 | 禁止生产使用 | 小项目可用 | 生产推荐 |
十三、最佳实践清单
- 禁止使用默认线程池:生产环境必须自定义线程池
- 设置线程名前缀:方便日志排查和线程 dump 分析
- 按业务隔离线程池:避免不同业务互相影响
- 合理设置队列容量:不要用无界队列(Integer.MAX_VALUE)
- 选择合适的拒绝策略:CallerRunsPolicy 最安全,不丢任务
- 异步方法内必须 try-catch:防止异常被吞
- 配置优雅关闭 :
setWaitForTasksToCompleteOnShutdown(true) - 监控线程池状态:活跃线程数、队列大小、拒绝次数
- 注意事务不传播:异步方法需要自己管理事务
- 避免自调用:@Async 方法必须通过代理调用才生效