【Spring】定時任務@Scheduled之單線程多線程問題

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());
        });
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章