@Scheduled默认是单线程串行执行
1. 默认执行模式 - 串行执行
java
// Spring Boot 2.x及更高版本的默认行为
public class TaskSchedulingAutoConfiguration {
// 默认情况下,Spring会创建一个单线程的任务调度器
// 线程名通常为:scheduling-1
}
关键点:
- Spring Boot 2.x+ 默认使用
ThreadPoolTaskScheduler,但线程池大小为1 - 所有
@Scheduled方法都在同一个线程中串行执行 - 如果前一个任务未完成,后续任务会等待
2. 多类多方法的执行情况示例
java
@Component
public class TaskA {
@Scheduled(cron = "0 30 * * * *") // 每小时的第30分钟
public void taskA1() {
System.out.println(Thread.currentThread().getName() + " - TaskA1执行");
// 假设执行需要10分钟
}
}
@Component
public class TaskB {
@Scheduled(cron = "0 30 * * * *") // 同时间触发
public void taskB1() {
System.out.println(Thread.currentThread().getName() + " - TaskB1执行");
}
}
执行时序图:
时间轴:
│
├─ 00:30:00 - 任务A1开始执行(scheduling-1线程)
│ ├─ 执行10分钟...
│ └─ 00:40:00 - 任务A1结束
│
├─ 00:40:00 - 任务B1开始执行(同一线程scheduling-1)
│ └─ 延迟10分钟执行!
3. 调度逻辑详解
触发条件
- 所有配置了相同cron表达式的任务会在同一分钟触发(本例中的每小时30分)
- 但执行顺序是不确定的 ,取决于:
- Spring Bean的加载顺序
- 方法发现的顺序
- 但通常按字母顺序或包结构
阻塞问题
java
// 如果任务执行时间超过调度间隔,会出现阻塞
@Scheduled(cron = "0 */5 * * * *") // 每5分钟执行一次
public void longRunningTask() {
System.out.println("开始执行 - " + new Date());
try {
Thread.sleep(10 * 60 * 1000); // 执行10分钟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束执行 - " + new Date());
}
// 结果:后续调度会被跳过,因为前一个任务还在运行
4. 配置并行执行
方案1:增加调度器线程数(Spring Boot配置)
yaml
# application.yml
spring:
task:
scheduling:
pool:
size: 10 # 设置线程池大小
thread-name-prefix: "scheduling-"
方案2:自定义TaskScheduler
java
@Configuration
@EnableScheduling
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10); // 并行执行数量
taskScheduler.setThreadNamePrefix("custom-scheduler-");
taskScheduler.setAwaitTerminationSeconds(60);
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
taskScheduler.initialize();
taskRegistrar.setTaskScheduler(taskScheduler);
}
}
方案3:使用@Async实现异步执行
java
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
}
@Component
public class AsyncTask {
@Async // 添加此注解
@Scheduled(cron = "0 30 * * * *")
public void asyncTaskMethod() {
// 每个调度都会在新线程中执行
}
}
5. 不同场景的执行对比
| 场景 | 执行方式 | 特点 | 适用场景 |
|---|---|---|---|
| 默认 | 串行执行 | 单线程,任务会排队 | 简单任务,执行时间短 |
| 增加pool.size | 并行执行 | 多线程,同时执行多个任务 | 多个独立任务需要同时执行 |
| @Async + @Scheduled | 异步并行 | 每次调度都创建新线程 | 任务执行时间长,需要完全隔离 |
| 自定义TaskScheduler | 灵活控制 | 可精细控制线程池行为 | 复杂调度需求 |
6. 最佳实践建议
java
@Component
@Slf4j
public class BestPracticeScheduler {
// 1. 固定延迟 - 适合需要间隔执行的任务
@Scheduled(fixedDelay = 5000) // 上次执行结束后5秒再执行
public void taskWithFixedDelay() {
// 适合需要保证执行间隔的场景
}
// 2. 固定频率 - 适合周期性任务
@Scheduled(fixedRate = 5000) // 每5秒执行一次(不等待上次完成)
public void taskWithFixedRate() {
// 使用@Async避免阻塞
}
// 3. Cron表达式 - 适合定时任务
@Async // 建议添加@Async避免阻塞其他任务
@Scheduled(cron = "0 30 * * * *")
public void cronTask() {
try {
// 业务逻辑
} catch (Exception e) {
log.error("调度任务执行失败", e);
}
}
// 4. 初始延迟
@Scheduled(fixedRate = 5000, initialDelay = 10000)
public void taskWithInitialDelay() {
// 应用启动10秒后开始执行,之后每5秒执行一次
}
}
7. 监控和调试
java
// 监控调度执行情况
@Component
@Slf4j
public class SchedulerMonitor {
@Scheduled(cron = "0 30 * * * *")
public void monitoredTask() {
long startTime = System.currentTimeMillis();
try {
// 业务逻辑
} finally {
long duration = System.currentTimeMillis() - startTime;
if (duration > 60000) { // 超过1分钟
log.warn("调度任务执行时间过长: {}ms", duration);
}
}
}
}
总结
- 默认是串行执行 - 单线程,任务会排队
- 多个类的相同cron任务 - 会在同一时间触发,但串行执行
- 配置并行执行 - 通过
spring.task.scheduling.pool.size或自定义TaskScheduler - 最佳实践 - 长任务建议使用
@Async,短任务可保持默认 - 注意 - 并行执行时需要考虑线程安全和资源竞争问题