計划任務功能在應用程序及其常見,使用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接口,並在重寫的方法中自定義一個線程池。