Spring Boot 中 @Async 与 @Scheduled 的线程池配置与常见问题

891 阅读3分钟

一、默认行为:不配置线程池的风险

1. @Async 的默认线程池

当使用 @Async 注解时,若未显式指定线程池,Spring Boot 会使用 SimpleAsyncTaskExecutor
特点

  • 每个异步任务都会创建一个新线程。

  • 线程不重用,任务结束后线程销毁。
    潜在问题

@Async
public void asyncTask() {
    // 耗时操作
}
  • 资源耗尽:高并发场景下频繁创建线程,导致内存和 CPU 资源耗尽。
  • 性能下降:线程创建和销毁的开销显著,影响系统吞吐量。

2. @Scheduled 的默认线程池

当使用 @Scheduled 注解时,默认使用 Executors.newSingleThreadScheduledExecutor()
特点

  • 单线程执行所有定时任务。

  • 任务按顺序执行,无并发。
    潜在问题

@Scheduled(fixedRate = 1000)
public void scheduledTask() {
    // 耗时操作
}
  • 任务阻塞:某个任务执行时间过长,后续任务会被延迟。
  • 调度失效:单线程崩溃会导致所有定时任务终止。

二、问题场景:未配置线程池的典型 Bug

1. 异步任务资源耗尽

场景:某个接口调用触发大量异步任务。
现象

  • 日志中出现 OutOfMemoryError: unable to create new native thread
  • CPU 使用率飙升,系统响应变慢。
    原因
  • SimpleAsyncTaskExecutor 无限制创建线程,耗尽系统资源。

2. 定时任务调度延迟

场景:多个定时任务共享单线程。
现象

  • 任务实际执行间隔远大于配置的 fixedRate

  • 监控显示任务排队堆积。
    代码示例

@Scheduled(fixedRate = 1000)
public void task1() { 
    Thread.sleep(2000); // 阻塞 2 秒
}

@Scheduled(fixedRate = 1000)
public void task2() { 
    // 被 task1 阻塞,延迟执行
}

3. 线程上下文污染

场景:异步任务依赖 ThreadLocal 上下文(如用户认证信息)。
现象

  • 异步任务中获取不到主线程的 ThreadLocal 数据。
    原因
  • 默认线程池每次创建新线程,ThreadLocal 数据未传递。

三、解决方案:显式配置线程池

1. 配置异步任务线程池

步骤

  1. 定义线程池 Bean:

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean("customAsyncExecutor")
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        return executor;
    }
}
  1. 在异步方法中指定线程池:

@Async("customAsyncExecutor")
public void asyncTask() {
    // 任务逻辑
}

避坑指南

  • 避免在同一个类中调用 @Async 方法(需通过代理调用)。

2. 配置定时任务线程池

步骤

  1. 定义线程池 Bean:

@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(4));
    }
}
  1. 定时任务自动使用线程池:

@Scheduled(fixedRate = 1000)
public void scheduledTask() {
    // 任务逻辑
}

避坑指南

  • 确保 @Scheduled 方法执行时间小于调度间隔。

3. 线程上下文传递

场景:异步任务需要获取主线程的上下文(如用户信息)。
解决方案:使用 TaskDecorator 传递 ThreadLocal。

@Bean("customAsyncExecutor")
public Executor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // ... 其他配置
    executor.setTaskDecorator(new ContextCopyingDecorator());
    return executor;
}

public class ContextCopyingDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        // 捕获主线程上下文
        Map<String, Object> context = ThreadLocalContext.getContext();
        return () -> {
            try {
                ThreadLocalContext.setContext(context);
                runnable.run();
            } finally {
                ThreadLocalContext.clear();
            }
        };
    }
}

四、监控与调优

1. 监控线程池状态

通过 Actuator 暴露线程池指标:

management:
  endpoints:
    web:
      exposure:
        include: metrics,threadpools

访问 /actuator/metrics 查看线程池活跃线程数、队列大小等。

2. 动态调整线程池参数

结合配置中心(如 Spring Cloud Config),动态修改线程池参数:

@RefreshScope
@Bean("customAsyncExecutor")
public Executor asyncExecutor(
    @Value("${async.core-pool-size:4}") int corePoolSize,
    @Value("${async.max-pool-size:8}") int maxPoolSize
) {
    // 初始化线程池
}

五、常见错误与修复

1. 异步方法不生效

原因:未在主类或配置类添加 @EnableAsync
修复

@SpringBootApplication
@EnableAsync // 添加此注解
public class Application { ... }

2. 定时任务阻塞

现象:多个 @Scheduled 任务串行执行。
修复:显式配置多线程池,或为任务指定不同线程池。

3. 线程池拒绝策略不当

现象:日志中出现 RejectedExecutionException
修复:配置合理的拒绝策略:

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());