栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Spring Scheduled 定时任务没有准时执行

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Spring Scheduled 定时任务没有准时执行

1、问题现象


可以看到同一个定时任务,每天的执行开始时间都是不同的。这个定时任务正确的开始执行时间是0点开始执行,但是有的在执行的时候都是2点了。真正开始执行时间和设置的定时时间偏差较大。
这个是真实配置的定时时间。

2、问题溯源

从问题现象的图上面可知道,每次定时任务延迟的时间都不一样,而且有时是准确执行,有时是延迟执行。且定时任务能正常执行,但是执行的时间和配置的时间不对,也就是说配置是没问题,有问题可能是Spring的scheduled问题,那么Spring的scheduled是如何处理定时任务的呢,这时就要带着这个问题去看Spring处理scheduled的源码了。
首先DeBug启动一个定时任务,看他的整个调用链。看到其调用链类似这样。

可以看到整个调用链起始地方,即Spring启动的流程方法,refresh()(整个Spring启动流程最核心的方法)。
然后在往下找,会看到finshRefresh(),这方法表示整个Spring的启动流程基本上完成,在这个方法中,会调用一些监听Spring初始化完成ApplicationContext容器的监听者。而Spring的Scheduled就是依赖Spring的监听器模式来实现定时任务的发现和注册。
下面直接调到采用依赖Spring的监听器模式处理定时任务的发现和注册的地方
org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#onApplicationEvent。

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    if (event.getApplicationContext() == this.applicationContext) {
        // Running in an ApplicationContext -> register tasks this late...
        // giving other ContextRefreshedEvent listeners a chance to perform
        // their work at the same time (e.g. Spring Batch's job registration).
        finishRegistration();
    }
}

下面就是出现上面这种场景的问题所在了,在finishRegistration()方法中,会对taskScheduled这个属性设置值,这个本质上就是一个线程池,用来处理处理定时任务的。正常来说,如果程序中没有指定线程池的配置,也就是Spring的Scheduled的默认线程池配置,其线程池的线程数默认为1,也就是说默认情况下,Spring用来处理定时任务的线程只有一个。如果有定时的处理时间占用时间比较长,那么就会导致下一个定时任务,即使到达了配置的定时时间,也不会立即执行,而是等到前面一个任务处理完成了,才会进行处理。

Spring中Scheduled使用的线程池实际上就是ScheduledThreadPoolExecutor这个线程池,由于只有一个线程处理任务,且是定时任务,因此Spring在启动时只是将定时任务添加到队列中。等真正的任务时间到了之后在交由工作线程进行处理。启动时,将任务添加到队列的位置在java.util.concurrent.ThreadPoolExecutor#ensurePrestart。

void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}
3、问题复现

那么,既然发现了问题所在,我们可以复现一下,到底是不是咱们发现的这个问题。下面是我测试的demo代码。

@Configuration
public class TaskDemo {
    @Scheduled(cron = "0 56 15 * * ?")
    public void test() throws InterruptedException {
        System.out.println("11111:" + LocalDateTime.now().toString());
        Thread.sleep(2 * 60 * 1000);
    }

    @Scheduled(cron = "0 57 15 * * ?")
    public void test1() {
        System.out.println("222:" + LocalDateTime.now().toString());
    }
}

输出结果为:

按照理想情况下,我配置的定时任务test和test1的执行时间应该都是准时的。但是实际上执行时间,因为在test这个定时任务中进行了延迟睡眠2min,而test1的实际执行时间是在test最终执行完成后,才进行执行的。

4、解决方案

既然,默认的Scheduled的线程池中线程的数量为1,那么我们不妨将其增大,让更多的线程来处理定时任务即可。而Spring的Scheduled提供了对线程池的处理扩展。
回到上面说到的设置taskScheduled的地方,即finishRegistration()方法出,在这个方法中,有个判断逻辑即获取有没有或者实现SchedulingConfigurer的bean。如果有的话可以在SchedulingConfigurer中对register的属性进行设置,而taskScheduled就是register的一个属性。而SchedulingConfigurer中就一个方法。而且会在finishRegistration()中执行SchedulingConfigurer的configureTasks(ScheduledTaskRegistrar taskRegistrar)方法。

private void finishRegistration() {
    if (this.scheduler != null) {
        this.registrar.setScheduler(this.scheduler);
    }

    if (this.beanFactory instanceof ListableBeanFactory) {
        Map beans =
            ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
        List configurers = new ArrayList<>(beans.values());
        AnnotationAwareOrderComparator.sort(configurers);
        for (SchedulingConfigurer configurer : configurers) {
            configurer.configureTasks(this.registrar);
        }
    }
    //......省略
    this.registrar.afterPropertiesSet();
}
@FunctionalInterface
public interface SchedulingConfigurer {

	
	void configureTasks(ScheduledTaskRegistrar taskRegistrar);

}

下面是我加了配置的一个demo。在这个demo中,我将线程池的核心线程数设置为10。

@Configuration
public class TestConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
    }
}

加完配置之后的执行结果:

@Configuration
public class TaskDemo {

    @Scheduled(cron = "0 03 16 * * ?")
    public void test() throws InterruptedException {
        System.out.println("11111:" + LocalDateTime.now().toString());
        Thread.sleep(2 * 60 * 1000);
    }

    @Scheduled(cron = "0 04 16 * * ?")
    public void test1() {
        System.out.println("222:" + LocalDateTime.now().toString());
    }
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/859422.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号