Spring Boot 计划任务中的一个“坑”
计划任务功能在应用程序及其常见,使用Spring Boot的@Scheduled 注解可以很方便的定义一个计划任务。然而在实际开发过程当中还应该注意它的计划任务默认是放在容量为1个线程的线程池中执行,即任务与任务之间是串行执行的。如果没有注意这个问题,开发的应用可能出现不按照设定计划执行的情况。本文将介绍几种增加定时任务线程池的方式。
验证Spring Boot计划任务中的“坑”
其实是验证Spring Boot 中执行任务的线程只有1个。先定义两个任务,输出任务执行时的线程ID,如果ID一样则认为这两个任务是一个线程执行的。这意味着,某个任务即使到达了执行时间,如果还有任务没有执行完,它也不能开始。
package com.example.springbootlearning.schedule; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component @EnableScheduling public class ScheduleDemo { @Scheduled(fixedRate = 5000) public void work1() throws InterruptedException { System.out.println(System.currentTimeMillis() + ":" + Thread.currentThread().getId() + ": work1 Begin."); Thread.sleep(3000L); System.out.println(System.currentTimeMillis() + ":" + Thread.currentThread().getId() + ": work1 End."); } @Scheduled(fixedRate = 5000) public void work2() throws InterruptedException { System.out.println(System.currentTimeMillis() + ":" + Thread.currentThread().getId() + ": work2 Begin."); Thread.sleep(3000L); System.out.println(System.currentTimeMillis() + ":" + Thread.currentThread().getId() + ": work2 End."); } }
部分输出:
1577949369664:20: work2 Begin. 1577949371665:20: work2 End. 1577949371665:20: work1 Begin. 1577949373665:20: work1 End. ......
以上代码定义了两个任务work1和work2,都是每5000ms执行一次,输出开始执行时间戳,线程ID,和结束时间戳。从输出可以看出,执行两个work的线程是相同的(ID都是20),work2先抢占线程资源,work2 执行结束之后 work1才开始执行。这导致:本来按计划work1应该每5秒执行一次,现在却变成了每6秒执行一次。如图:
让计划任务在不同的线程中执行
要让计划任务在不同的线程中执行,只需要自己去定义执行任务的线程池就可以了。具体操作就是添加一个实现了 SchedulingConfigurer 接口的配置类,然后在配置类里面设置线程池。具体代码如下:
package com.example.springbootlearning.schedule; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import java.util.concurrent.Executors; @Configuration public class ScheduleConfiguration implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) { // 将 Scheduler 设置为容量为 5 的计划任务线程池。 scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }
Spring Boot 版本2.1.0之后,可以直接在配置文件中设置 spring.task.scheduling.pool.size 的值来配置计划任务线程池的容量,而不需要额外再写一个类。
spring.task.scheduling.pool.size=5
输出:
1577953149351:20: work2 Begin.
1577953149352:21: work1 Begin.
从输出可以看出,两个任务可以认为是同时开始的(相差1毫秒),分别在ID为20和ID为21的线程中执行。如图:
小结
Spring Boot 定时任务的线程数默认是1,直接使用它调用定时任务会导致不同的任务无法并行执行。因此在使用它之前应该根据需求在application.properties中修改它的线程池容量;或者实现SchedulingConfigurer接口,并在重写的方法中自定义一个线程池。