在 Spring 中,我们可以使用 @Scheduled 方便的进行定时任务的执行,其支持以下三种调度方式:Cron、FixedDelay、FixedRate。下面分别介绍在标准模式下和异步模式下这三种调度方式的不同。
一、标准模式
1.1 示例准备
创建一个 SpringBoot 初始程序,依赖包只需要引入 spring-boot-starter-web 即可:
1 2 3 4 5 6
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
|
创建类 SchedulerConfig,实现 SchedulingConfigurer 接口,作用是自定义 @Scheduled 执行的线程池配置信息。
1 2 3 4 5 6 7 8 9 10 11
| @Configuration public class SchedulerConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(10); taskScheduler.initialize(); taskRegistrar.setTaskScheduler(taskScheduler); } }
|
为了方便起见,示例的定时任务直接定义在主类中,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @EnableScheduling @SpringBootApplication public class DemoApplication {
public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }
public void job() throws InterruptedException { LocalTime start = LocalTime.now();
System.out.println(Thread.currentThread().getName() + " start @ " + start); Thread.sleep(ThreadLocalRandom.current().nextInt(8) * 1000);
LocalTime end = LocalTime.now(); System.out.println(Thread.currentThread().getName() + " end @ " + end + ", seconds cost " + (ChronoUnit.SECONDS.between(start, end))); } }
|
至此我们只需要给 job() 方法上方加上 @Scheduled 注解就可以实现定时执行了。
1.2 Cron
Cron 本身语法比较简单和通用,具体语法看文章《详解 Cron 表达式》。以每隔 5 秒执行为例,给 job() 方法上方加上注解:
1
| @Scheduled(cron = "0/5 * * * * ? ")
|
程序运行结果如下:
1 2 3 4 5 6 7
| ThreadPoolTaskScheduler-1 start @ 14:51:05.004 ThreadPoolTaskScheduler-1 end @ 14:51:08.008, seconds cost 3 ThreadPoolTaskScheduler-2 start @ 14:51:10.003 ThreadPoolTaskScheduler-2 end @ 14:51:16.007, seconds cost 6 ThreadPoolTaskScheduler-1 start @ 14:51:20.004 ThreadPoolTaskScheduler-1 end @ 14:51:23.008, seconds cost 3 ...
|
首次执行时间为 14:51:05,本次执行耗时 3 秒。
第二次计划执行时间为 14:51:10,按时执行,本次执行耗时 6 秒。
第三次计划执行时间为 14:51:15,由于第二次执行直到 14:51:16 才执行完毕,因此本次不执行,等到下次 14:51:20 执行。
由此可见,Cron 方式严格按照执行周期执行,如果到了下次执行周期时,而本次执行未执行完毕,直接跳过。
1.3 FixedDelay
以程序启动后延时 1 秒执行,后续间隔 5 秒执行为例,给 job() 方法上方加上注解:
1
| @Scheduled(initialDelay = 1000, fixedDelay = 5000)
|
程序运行结果如下:
1 2 3 4 5 6 7
| ThreadPoolTaskScheduler-3 start @ 15:00:12.106 ThreadPoolTaskScheduler-3 end @ 15:00:16.109, seconds cost 4 ThreadPoolTaskScheduler-2 start @ 15:00:21.113 ThreadPoolTaskScheduler-2 end @ 15:00:28.117, seconds cost 7 ThreadPoolTaskScheduler-4 start @ 15:00:33.121 ThreadPoolTaskScheduler-4 end @ 15:00:36.126, seconds cost 3 ...
|
首次执行时间为 15:00:12,本次执行耗时 4 秒。
第二次计划执行时间为第一次执行结束时间 + 5 秒,即 15:00:21,本次执行耗时 6 秒。
第三次计划执行时间为第二次执行结束时间 + 5 秒,即 15:00:33。
由此可见,fixedDelay 将在上一次执行完毕后,等待指定时间后再进行下次执行,任务的执行间隔是固定的,不受执行时长的影响。
1.4 FixedRate
以程序启动后延时 1 秒执行,后续间隔 5 秒执行为例,给 job() 方法上方加上注解:
1
| @Scheduled(initialDelay = 1000, fixedRate = 5000)
|
程序运行结果如下:
1 2 3 4 5 6 7 8 9
| ThreadPoolTaskScheduler-3 start @ 15:21:18.883 ThreadPoolTaskScheduler-3 end @ 15:21:18.883, seconds cost 0 ThreadPoolTaskScheduler-2 start @ 15:21:23.883 ThreadPoolTaskScheduler-2 end @ 15:21:29.887, seconds cost 6 ThreadPoolTaskScheduler-4 start @ 15:21:29.888 ThreadPoolTaskScheduler-4 end @ 15:21:31.892, seconds cost 2 ThreadPoolTaskScheduler-1 start @ 15:21:33.880 ThreadPoolTaskScheduler-1 end @ 15:21:35.882, seconds cost 2 ...
|
和 fixedDelay 不同,fixedRate 在任务启动后,根据首次执行开始的时间将后续要执行的任务进行预先编排,以上面输出为例。首次执行开始时间为 15:21:18,那么编排如下:
| 编排执行顺序 |
编排执行时间 |
| T1 |
15:21:18 |
| T2 |
15:21:23 |
| T3 |
15:21:28 |
| T4 |
15:21:33 |
T1 执行时间为15:21:18,本次执行耗时 0 秒。
T2 计划执行时间为 15:21:23,由于 T1 到 15:21:18 就执行结束了,因此等待到该时刻后执行即可。本次执行耗时 6 秒。
T3 计划执行时间为 15:21:28,但由于 T2 直到 15:21:29 才执行结束,比预计执行时间还要晚。因此直接执行。本次执行耗时 2 秒。
T4 计划执行时间为 15:21:33,由于 T3 到 15:21:31 就执行结束了,因此等待到该时刻后执行即可。本次执行耗时 2 秒。
由此可见,fixedRate 在上次任务执行结束后,根据编排时间表决定是直接执行还是等待执行下次任务。如果预先编排的时间晚于上次执行时间 + fixedRate值,则等待到预定执行时间,否则立即执行。
1.5 总结

- Cron: 在预计的执行时间上,上次执行还未结束,就跳过。
- fixedDelay: 总是在上次执行完毕后,等待 n 秒后执行下次。
- fixedRate: 上次执行完毕后,比较编排时间和当前时间。如果超过编排时间,立即执行;如果未到编排时间,等待到编排时间。
| 调度方式 |
一言以蔽之 |
| cron |
一旦错过就不在 |
| fixedDelay |
老死不相往来 |
| fixedRate |
欠的总是要还的 |
二、异步模式
Spring 提供 @Async 注解实现了方法的异步执行,使用该注解的方法会在独立的线程中执行。也就是说使用了该注解的方法,就不受上文定义的 SchedulerConfig 的管理了。
2.1 示例准备
修改主类,新增 @EnableAsync 注解,同时给 job() 方法添加 @Async 注解,这样该方法就会以异步方式执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @EnableAsync @EnableScheduling @SpringBootApplication public class DemoApplication {
public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }
@Async public void job() throws InterruptedException { LocalTime start = LocalTime.now();
System.out.println(Thread.currentThread().getName() + " start @ " + start); Thread.sleep(ThreadLocalRandom.current().nextInt(8) * 1000);
LocalTime end = LocalTime.now(); System.out.println(Thread.currentThread().getName() + " end @ " + end + ", seconds cost " + (ChronoUnit.SECONDS.between(start, end))); } }
|
2.1 Cron
给 job() 方法上方加上注解:
1
| @Scheduled(cron = "0/5 * * * * ? ")
|
程序运行结果如下:
1 2 3 4 5 6 7 8 9
| task-1 start @ 15:59:35.006 task-1 end @ 15:59:38.011, seconds cost 3 task-2 start @ 15:59:40.005 task-3 start @ 15:59:45.004 task-3 end @ 15:59:46.006, seconds cost 1 task-2 end @ 15:59:47.008, seconds cost 7 task-4 start @ 15:59:50.004 task-4 end @ 15:59:55.005, seconds cost 5 ...
|
首次执行时间为 15:59:35,本次执行耗时 3 秒。
第二次计划执行时间为 15:59:40,按时执行,本次执行耗时 7 秒。
第三次计划执行时间为 15:59:45,由于第二次执行直到 15:59:47 才执行完毕,因此另开线程执行。
由此可见,异步模式下的 Cron 方式严格按照执行周期执行,如果到了下次执行周期时,而本次执行未执行完毕,从线程池中取新的线程去执行。
2.3 FixedDelay
以程序启动后延时 1 秒执行,后续间隔 5 秒执行为例,给 job() 方法上方加上注解:
1
| @Scheduled(initialDelay = 1000, fixedDelay = 5000)
|
程序运行结果如下:
1 2 3 4 5 6 7
| task-1 start @ 16:14:45.557 task-1 end @ 16:14:45.558, seconds cost 0 task-2 start @ 16:14:50.551 task-3 start @ 16:14:55.556 task-2 end @ 16:14:57.555, seconds cost 7 task-3 end @ 16:15:00.559, seconds cost 5 ...
|
首次执行时间为 16:14:45,本次执行耗时 0 秒。
第二次计划执行时间为第一次执行开始时间 + 5 秒,即 16:14:50,本次执行耗时 7 秒。
第三次计划执行时间为第二次执行开始时间 + 5 秒,即 16:14:55。由于第二次执行直到 16:14:57 才执行完毕,因此另开线程执行。
由此可见,fixedDelay 将在上一次执行开始后,等待指定时间后再进行下次执行,如果到了下次执行周期时,而本次执行未执行完毕,从线程池中取新的线程去执行。
注意,异步模式下的 fixedDelay 感觉变成了 fixedRate 的感觉,竟然是根据任务开始时间而不是任务结束时间了???
2.4 FixedRate
以程序启动后延时 1 秒执行,后续间隔 5 秒执行为例,给 job() 方法上方加上注解:
1
| @Scheduled(initialDelay = 1000, fixedRate = 5000)
|
程序运行结果如下:
1 2 3 4 5 6 7 8 9
| task-1 start @ 16:21:27.164 task-1 end @ 16:21:29.168, seconds cost 2 task-2 start @ 16:21:32.150 task-3 start @ 16:21:37.149 task-2 end @ 16:21:38.150, seconds cost 6 task-4 start @ 16:21:42.151 task-4 end @ 16:21:43.155, seconds cost 1 task-3 end @ 16:21:44.150, seconds cost 7 ...
|
还是根据标准模式一样,根据首次执行时间,进行任务编排:
| 编排执行顺序 |
编排执行时间 |
| T1 |
16:21:27 |
| T2 |
16:21:32 |
| T3 |
16:21:37 |
| T4 |
16:21:42 |
T1 执行时间为 16:21:27,本次执行耗时 2 秒。
T2 计划执行时间为 16:21:32,由于 T1 到 16:21:29 就执行结束了,因此等待到该时刻后执行即可。本次执行耗时 6 秒。
T3 计划执行时间为 16:21:37,但由于 T2 直到 16:21:38 才执行结束,因此另开线程执行。本次执行耗时 7 秒。
T4 计划执行时间为 16:21:42,但由于 T3 直到 16:21:44 才执行结束,因此另开线程执行。本次执行耗时 1 秒。
由此可见,fixedRate 在上次任务执行结束后,根据编排时间表决定是直接执行还是等待执行下次任务。如果预先编排的时间晚于上次执行时间 + fixedRate 值,则等待到预定执行时间,否则另开线程执行。
2.5 总结
首先说明下定时任务的异步模式用的很少,@Async 主要还是给非定时任务用的比较多。定时任务在该模式下,三种方式的调度逻辑一致,即本次执行时,上次执行是否完毕,如果完毕直接执行;如果未完毕,新开线程执行。