Spring @Scheduled注解调度机制详解

@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分)
  • 执行顺序是不确定的 ,取决于:
    1. Spring Bean的加载顺序
    2. 方法发现的顺序
    3. 但通常按字母顺序或包结构
阻塞问题
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);
            }
        }
    }
}

总结

  1. 默认是串行执行 - 单线程,任务会排队
  2. 多个类的相同cron任务 - 会在同一时间触发,但串行执行
  3. 配置并行执行 - 通过spring.task.scheduling.pool.size或自定义TaskScheduler
  4. 最佳实践 - 长任务建议使用@Async,短任务可保持默认
  5. 注意 - 并行执行时需要考虑线程安全和资源竞争问题
相关推荐
⑩-2 小时前
Blocked与Wati的区别
java·开发语言
AAA简单玩转程序设计2 小时前
救命!Java这3个小技巧,写起来爽到飞起✨
java
IManiy2 小时前
Java表达式引擎技术选型分析(SpEL、QLExpress)
java·开发语言
历程里程碑2 小时前
C++ 17异常处理:高效捕获与精准修复
java·c语言·开发语言·jvm·c++
雨雨雨雨雨别下啦2 小时前
ssm复习总结
java·开发语言
qq_356196952 小时前
Day 43图像数据与显存机制@浙大疏锦行
python
yaoh.wang2 小时前
力扣(LeetCode) 94: 二叉树的中序遍历 - 解法思路
python·算法·leetcode·面试·职场和发展·二叉树·跳槽
摸鱼仙人~2 小时前
Flask-SocketIO 连接超时问题排查与解决(WSL / 虚拟机场景)
后端·python·flask
haiyu_y2 小时前
Day 45 预训练模型
人工智能·python·深度学习