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