SpringBoot使用@scheduled定時執行任務的時候是在一個單線程中,如果有多個任務,其中一個任務執行時間過長,則有可能會導致其他後續任務被阻塞直到該任務執行完成。也就是會造成一些任務無法定時執行的錯覺。
可以通過如下代碼進行測試:
@Scheduled(cron = "0/1 * * * * ? ")
public void deleteFile() throws InterruptedException {
log.info("111delete success, time:" + new Date().toString());
Thread.sleep(1000 * 5);//模擬長時間執行,比如IO操作,http請求
}
@Scheduled(cron = "0/1 * * * * ? ")
public void syncFile() {
log.info("222sync success, time:" + new Date().toString());
}
/**輸出如下:
[pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:13 CST 2018
[pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:18 CST 2018
[pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:19 CST 2018
[pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:24 CST 2018
[pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:25 CST 2018
[pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:25 CST 2018
上面的日誌中可以明顯的看到syncFile被阻塞了,直達deleteFile執行完它才執行了
而且從日誌信息中也可以看出@Scheduled是使用了一個線程池中的一個單線程來執行所有任務的。
**/
/**如果把Thread.sleep(1000*5)註釋了,輸出如下:
[pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:04 CST 2018
[pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:04 CST 2018
[pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:05 CST 2018
[pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:05 CST 2018
[pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:06 CST 2018
[pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:06 CST 2018
這下正常了
**/
我估計是在定時任務的配置中設定了一個SingleThreadScheduledExecutor,查看源碼,從ScheduledAnnotationBeanPostProcessor類開始一路找下去。果然,在ScheduledTaskRegistrar(定時任務註冊類)中的ScheduleTasks中又這樣一段判斷:
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
這就說明如果taskScheduler爲空,那麼就給定時任務做了一個單線程的線程池,正好在這個類中還有一個設置taskScheduler的方法:
public void setScheduler(Object scheduler) {
Assert.notNull(scheduler, "Scheduler object must not be null");
if (scheduler instanceof TaskScheduler) {
this.taskScheduler = (TaskScheduler) scheduler;
}
else if (scheduler instanceof ScheduledExecutorService) {
this.taskScheduler = new ConcurrentTaskScheduler(((ScheduledExecutorService) scheduler));
}
else {
throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
}
}
解決辦法
1、擴大原定時任務線程池中的核心線程數
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(50));
}
}
這個方法,在程序啓動後,會逐步啓動50個線程,放在線程池中。每個定時任務會佔用1個線程。但是相同的定時任務,執行的時候,還是在同一個線程中。
例如,程序啓動,每個定時任務佔用一個線程。任務1開始執行,任務2也開始執行。如果任務1卡死了,那麼下個週期,任務1還是處理卡死狀態,任務2可以正常執行。也就是說,任務1某一次卡死了,不會影響其他線程,但是他自己本身這個定時任務會一直等待上一次任務執行完成!
2、把Scheduled配置成成多線程執行
@Configuration
@EnableAsync
public class ScheduleConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(50);
return taskScheduler;
}
}
@EnableScheduling
public class TaskFileScheduleService {
@Async
@Scheduled(cron="0 */1 * * * ?")
public void task1(){
.......
}
@Async
@Scheduled(cron="0 */1 * * * ?")
public void task2(){
.......
}
這種方法,每次定時任務啓動的時候,都會創建一個單獨的線程來處理。也就是說同一個定時任務也會啓動多個線程處理。
例如:任務1和任務2一起處理,但是線程1卡死了,任務2是可以正常執行的。且下個週期,任務1還是會正常執行,不會因爲上一次卡死了,影響任務1。
但是任務1中的卡死線程越來越多,會導致50個線程池佔滿,還是會影響到定時任務。
這時候,可能會幾個月發生一次~到時候再重啓就行了!
3、將@Scheduled註釋的方法內部改成異步執行
//當然了,構建一個合理的線程池也是一個關鍵,否則提交的任務也會在自己構建的線程池中阻塞
ExecutorService service = Executors.newFixedThreadPool(5);
@Scheduled(cron = "0/1 * * * * ? ")
public void deleteFile() {
service.execute(() -> {
log.info("111delete success, time:" + new Date().toString());
try {
Thread.sleep(1000 * 5);//改成異步執行後,就算你再耗時也不會印象到後續任務的定時調度了
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
@Scheduled(cron = "0/1 * * * * ? ")
public void syncFile() {
service.execute(()->{
log.info("222sync success, time:" + new Date().toString());
});
}