Spring @Async 与自定义线程池实战指南

Spring @Async 与自定义线程池实战指南

一、概述

@Async 是 Spring 提供的异步执行注解,可以将方法的执行从调用线程转移到独立的线程池中,实现非阻塞调用。常用于日志记录、消息通知、数据同步等不需要同步等待结果的场景。

@Async 如果不配合自定义线程池使用,会带来严重的生产隐患。本文详细介绍 @Async 的使用方式、为什么必须自定义线程池、线程池参数如何设计,以及常见陷阱。


二、示例场景

一个订单系统,支付成功后需要:

  1. 更新订单状态(同步,必须成功)
  2. 发送短信通知(异步,失败不影响主流程)
  3. 记录操作日志(异步,失败不影响主流程)

注:

博客:

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-sizequeue-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;
    }
}

十二、方案对比总结

维度 默认线程池 自定义单线程池 按业务隔离线程池
实现复杂度 零配置
资源可控性 精细
业务隔离
故障影响范围 全局 全局 局部
监控粒度 整体 按业务
推荐程度 禁止生产使用 小项目可用 生产推荐

十三、最佳实践清单

  1. 禁止使用默认线程池:生产环境必须自定义线程池
  2. 设置线程名前缀:方便日志排查和线程 dump 分析
  3. 按业务隔离线程池:避免不同业务互相影响
  4. 合理设置队列容量:不要用无界队列(Integer.MAX_VALUE)
  5. 选择合适的拒绝策略:CallerRunsPolicy 最安全,不丢任务
  6. 异步方法内必须 try-catch:防止异常被吞
  7. 配置优雅关闭setWaitForTasksToCompleteOnShutdown(true)
  8. 监控线程池状态:活跃线程数、队列大小、拒绝次数
  9. 注意事务不传播:异步方法需要自己管理事务
  10. 避免自调用:@Async 方法必须通过代理调用才生效
相关推荐
程序猿乐锅1 小时前
【MySQL | 第一篇】SQL语句怎么分?DDL、DML、DQL 一篇讲清楚
数据库·sql·mysql
键盘上的猫头鹰1 小时前
【MySQL 教程(五)】SQL函数详解:字符、数字、日期、转换与通用函数
数据库·mysql·数据分析
圣殿骑士-Khtangc1 小时前
Python列表、字典、集合高阶操作精讲:从基础到工程实战
开发语言·python
Gauss松鼠会2 小时前
GaussDB(DWS)数据融合:Oracle增量数据迁移到DWS
java·数据库·算法·oracle·性能优化·gaussdb
云和恩墨2 小时前
数据库一体机简史:德维特与微软的“复仇者联盟”
数据库·microsoft
hzp6662 小时前
PyCharm 中开发 Flask 应用时霸占 5000 端口
python·flask
aqiu1111112 小时前
面向对象OOP
python
ULIi096kr2 小时前
Redis 分布式锁进阶第七十四篇
数据库·redis·分布式
有梦想的小何2 小时前
库存快照报表升级实战:SQL 窗口函数 + 分区管理(MySQL 8.0)
数据库·sql·mysql