点击上方 "小码宋", 关注 "设为星标"
技术文章第一时间送达!
1.@Scheduled 注解
在 SpringBoot 项目中使用定时任务时可以使用 @Scheduled 标注在需要定时执行的方法上. 该注解位于 spring-context.jar 包中, 关于 @Scheduled 的具体描述如下:
注意:@Scheduled 注解要生效还需要在系统启动类或配置类上添加 @EnableScheduling 注解.
2. 简单使用 @Scheduled 注解
2.1 首先这里创建了一个普通的 SpringBoot 项目叫 SpringbootApplication, 在启动类上添加 @EnableSchedling 注解
- @SpringBootApplication
- @EnableScheduling
- public class SpringbootApplication {
- public static void main(String[] args) {
- SpringApplication.run(SpringbootApplication.class, args);
- }
- }
2.2 创建定时任务调度类 TestSchedule, 并定义 taskSchedule1 方法使用 @Scheduled(cron = "0/10 * * * * ?") 标注, 表示该方法从 0 秒开始, 每隔 10 秒执行一次, 方法内部获取了执行当前任务的线程, 打印了任务开始和结束的时间, 线程 ID, 线程的名字, sleep 5 秒表示任务执行完成需要花费 5 秒时间. 使用 @Component 注解, 交给 Spring 容器管理.
@Component public class TestSchedule { @Scheduled(cron = "0/10 * * * * ?") public void taskSchdule1() throws InterruptedException { Thread t = Thread.currentThread(); System.out.println("taskSchule1"+ new Date().toLocaleString() +"ThreadID:"+ t.getId() +" "+t.getName()); Thread.sleep(5000); System.out.println("taskSchule1 end"+ new Date().toLocaleString() +"ThreadID:"+ t.getId() +" "+t.getName()); } }
2.3 运行结果
到这里最简单的定时任务调度就算完成了, 启动项目, 打印结果如下:
运行结果正常, 每隔 10 秒执行一次, 每次执行花费 5 秒时间.
3. 第一个坑
我们现在在 TestSchedule 类中添加第二个需要任务调度方法, 每隔 3 秒执行一次.
@Scheduled(cron = "0/3 * * * * ?") public void taskSchdule2(){ Thread t = Thread.currentThread(); System.out.println("taskSchule2"+ new Date().toLocaleString() +"ThreadID:"+ t.getId() +" "+t.getName()); }
再次运行, 结果如下:
这里我们发现任务 2, 在 a,b 两处时间相差 7 秒, 已超过 3 秒, 显然 b 处是在任务 1 结束之后立刻执行的, 并且任务 1 和任务 2 都是同一个线程执行的.
因为: Spring 中 @EnableScheduling 和 @Scheduled 标注的定时任务默认是单线程执行的, 这里任务 1 执行任务需要花费较长时间, 所有阻塞了任务 2 的执行.
4. 使用 @Async 和 @EnableAsync 异步执行任务
事实上在 Spring 的定时任务包中提供了 @EnableAsync 和 @Async 注解用于多线程异步执行任务.
首先在启动类上添加 @EnableAsync 注解, 并在 TestSchedule 类上标注 @Async 注解, 表示该类中所有标注了 @Scheduled 的方法都使用异步处理方式.
再次运行项目, 结果如下:
此时, 任务 1 和任务 2 均运行正常, 并且任务 1 和任务 2 都是不同线程在执行, 不会出现任务之间相互阻塞的情况.
这里是解决了第一个坑的问题, 但是实际上可能引入第二个坑.
5. 第二个坑
这里我们稍作修改将任务的睡眠时间改成 11 秒 Thread.sleep(11000);, 此时任务 1 的执行时间已经超过了它的调度时间. 再次运行程序结果如下:
观察发现任务 2 正常执行, 但是任务 1 中 a,b 和 c,d 两组出现了交叉, 两组是不同线程执行的, 因为任务 1 的执行时间超过了调度时间, 所以, a 处开始执行, 在未执行完成的情况下, 任务的调度时间到了, 其他线程又立马调度了任务从 c 处开始执行.
这是使用 @EnableAsync 和 @Async 可能会出现的问题.
6. 解决坑 1 和坑 2
再次修改代码, 去掉 5 中的 @EnableAsync 和 @Async 注解, 去掉 2 中的 @EnableScheduling 注解,
创建一个任务配置类 ScheduleConfig 实现 SchedulingConfigurer 接口的 configureTasks 方法, 使用参数 taskRegistrar 为任务调度创建线程池
@Configuration @EnableScheduling public class ScheduleConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(taskExecutor()); } @Bean(destroyMethod="shutdown") public Executor taskExecutor() { return Executors.newScheduledThreadPool(10); } }
运行程序结果如下:
现在任务 1 和任务 2 均运行正常, 并且任务 1 不会出现坑 2 中的交叉现象, 任务 1 第二次调度会等到第一次调度执行完毕后的下一个调度时间点才会执行.
总结
SpringBoot 中可以使用 @EnableScheduling 和 @Scheduled 注解实现定时任务调度, 但是注意默认所有任务都被单个线程调度的, 有可能任务之间发生阻塞现象, 可以使用 @EnableAsync 和 @Async 注解实现异步多线程任务调度, 但需要注意任务执行时间如果大于任务调度周期时间, 可能出现同一个任务交叉执行的情况. 当然也可以使用第 6 步中的方法避免上述问题.
来源: http://www.tuicool.com/articles/eM3I3qR