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<String, SchedulingConfigurer> beans =
            ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
        List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
        AnnotationAwareOrderComparator.sort(configurers);
        for (SchedulingConfigurer configurer : configurers) {
            configurer.configureTasks(this.registrar);
        }
    }
    //......省略
    this.registrar.afterPropertiesSet();
}
@FunctionalInterface
public interface SchedulingConfigurer {

	/**
	 * Callback allowing a {@link org.springframework.scheduling.TaskScheduler
	 * TaskScheduler} and specific {@link org.springframework.scheduling.config.Task Task}
	 * instances to be registered against the given the {@link ScheduledTaskRegistrar}.
	 * @param taskRegistrar the registrar to be configured.
	 */
	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());
    }
}

在这里插入图片描述

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Scheduled定时任务是一种在Spring Boot中创建定时任务的方式。目前主要有三种创建方式: 1. 基于注解(@Scheduled)的静态任务:通过在方法上添加@Scheduled注解来指定任务执行时间。 2. 基于接口(SchedulingConfigurer)的动态任务:通过实现SchedulingConfigurer接口,可以根据数据库的内容动态调度任务。 3. 基于注解的多线程定时任务:通过使用@Scheduled注解和多线程来实现定时任务的并发执行。 在使用Spring Scheduled定时任务时,需要在启动类上添加@EnableScheduling注解来开启定时任务功能。然后可以在方法上使用@Scheduled注解来指定任务执行时间,或者实现SchedulingConfigurer接口来添加定时任务。同时,可以配置定时任务的多线程非阻塞运行,以提高任务的并发性能。 以上是关于Spring Scheduled定时任务的简要介绍和使用方式。如果需要更详细的信息,可以参考引用\[1\]和引用\[2\]中的内容。 #### 引用[.reference_title] - *1* [SpringBoot之Scheduled定时任务详解](https://blog.csdn.net/weixin_41003771/article/details/102655202)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [spring schedule定时任务详解](https://blog.csdn.net/qq_34480904/article/details/122410711)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值