咱们好,我是贰师兄,好久不见。因为贰师兄最近开端写聊聊Mysql这个专栏,为避免传递一些错误的常识,误导咱们,所以在刷Mysql的源码。这个进程消耗的时刻比较多,自然更新的速度也会慢一些,请咱们见谅。

 因为新的专栏还在准备中,本文咱们仍是聊一聊Spring的论题,本文咱们聊一下Spring守时使命调度。这个主题,信任咱们都比较了解,平时用的应该也比较多。不过说起完结原理,了解的小伙伴应该就不太多了,网上对应的文章尽管许多,不过根本都是运用教程。即便有少部分提及原理,说的也是含糊不清,更有甚者是胡说八道。

 其实Spring的守时使命调度,并不神秘,也不是Spring特有的,实质仍是凭借JDK的才能完结的,仅仅在运用办法上更Spring一点,也便是更简练、更便捷一点。

关于JDK的守时使命,主要是凭借 ScheduledThreadPoolExecutor完结的,感兴趣的小伙伴能够能够自行了解其完结原理。这儿咱们主要聊Spring的相关细节,关于JDK部分咱们不会详细介绍。

当然,假如你们有需求,恰巧我也有时刻的话,也能够拿出来说一说,究竟说啥不是说呢,是吧。

1. Spring守时使命的类型

 关于Spring守时使命的运用,小伙伴们应该都比较了解,便是直接在办法上加上@Scheduled注解即可。Spring会依据你指定的频率,守时调度该办法的履行,这一点完全不用你关心。

 运用自然是很简略的,这是Spring一向的风格。关于Spring是怎样做的,读过贰师兄前面文章的小伙伴应该也能够猜到,至少得先把这些标示了@Scheduled注解的守时办法找出来,然后在想办法让它守时履行。当然前面咱们也说了,这部分是凭借JDK的才能来完结的。

不了解这个路数的小伙伴,能够参阅聊透Spring工作机制中@EventListener注解的解析和注册进程,套路是相同的。

 可是在运用Spring守时调度的进程,也有一些细节需求先和小伙伴们介绍清楚。首要便是Spring支撑的三种类型使命的履行逻辑,这儿恐怕能说清楚的小伙伴们不多,尤其是在碰到单线程模型,咱们先整理一下:

Spring支撑CRON表达式类型、fixedDelay距离履行、fixedRate距离履行三种使命类型。关于这三种类型在使命履行上的差别,咱们一一介绍一下。

1.1 CRON表达式类型使命

 关于CRON表达式含义,这儿咱们不再介绍,信任小伙伴们都比较了解,真实不了解自行查阅材料吧。

 咱们要说的是,在单线程履行的状况下,假如CRON使命履行时刻过长,以至于下次履行的时刻都到了,可是上次使命还没有履行完毕,下次使命要怎样办。

 这儿先给定论:抛弃,也便是下一次使命履行就被抛弃了,也便是少履行了一次。这儿拿使命设置为每五秒履行一次的表达式,阐明一下:

  1. 假定10:00:00s时,使命榜首次履行,可是使命履行时刻很长,履行了7s。
  2. 依据使命的履行方案,10:00:05s时,应该要履行第2次使命,可是此刻发现有使命在履行(上一次使命需求履行到10:00:07s),那么,此次履行方案直接抛弃,也便是本次使命不履行了
  3. 依据使命的履行方案,10:00:10s时,应该要履行第三次使命,此刻发现没有使命履行,本次使命正常履行。

聊透Spring定时任务调度

 这儿咱们必定要留意,单线程模型下,因为榜首次使命履行时刻较长,导致第2次使命不履行,也便是少履行了一次。这儿或许会影响预期、从而产生事务影响。

1.2 fixedDelay距离类型使命

 fixedDelay是最简略的一种办法模型,距离履行:也便是推迟指定的距离后,再次履行下次使命。核算公式为:下次履行时刻 = 上次使命履行完毕时刻 + 距离时刻。相同的问题:假如使命履行时刻较长,下次履行时刻也会晚于预期。这儿以使命距离为五秒,阐明一下:

  1. 假定10:00:00s时,使命榜首次履行,使命履行时刻较长,履行了7s。
  2. 榜首次使命履行完毕后(10:00:07),等候5s后,再次履行下一次使命(10:00:12),第2次使命履行了3s。
  3. 第2次使命履行完毕后(10:00:15),等候5s后,再次履行下一次使命,依此类推。

聊透Spring定时任务调度

 这儿需求留意,单线程模型下,假如存在使命履行时刻较长,全体的履行方案都会往后顺延

1.3 fixedRate距离类型使命

 fixedRate也是距离履行的办法,仅仅这个距离不是依照使命完毕时刻核算的,而是依照开端时刻。核算公式为:下次履行时刻 = 上次使命履行开端时刻 + 距离时刻。当然,假如使命履行时刻较长,超过距离时刻,下次履行时刻也要顺延,究竟不能强暴的直接打断吧。

 不过fixedRate会将距离会主动缩小,尽量追赶方案履行时刻,一旦赶上或许追平,持续依照指定距离履行。这儿仍是以距离为五秒的状况,阐明一下:

  1. 假定10:00:00s时,使命榜首次履行,使命履行时刻较长,履行了7s。
  2. 依据使命的履行方案,10:00:05s时,应该要履行第2次使命,可是此刻榜首次使命还在履行中,所以第2次履行时刻只能等候顺延。
  3. 榜首次使命履行完毕后(10:00:07),发现现已晚于第2次履行的方案时刻了。会追赶进展,所以第2次使命立即履行。
  4. 这儿假定第2次使命只需求履行2s,在10:00:09就履行完毕了。方案第三次履行时刻为:10:00:10,也便是第2次使命现已追平了,无需持续追赶,此刻会遵从方案,在10:00:10时,正常履行第三次使命。

聊透Spring定时任务调度

 这儿需求留意,fixedRate会主动调整距离,使使命尽快追平方案时刻,追平后遵从方案履行。当然这儿评论的也是单线程模型下。

 好了,关于守时使命的三种类型的评论就这么多。咱们留意在单线程模型下,上面的评论才有含义。咱们清楚不同使命类型,发生使命履行时刻过长,对下次履行时刻的影响即可。再次着重,是单线程模型下,假如是多线程履行,影响状况需求结合线程池装备剖析了,这儿咱们不具有评论条件。

这儿为什么执着的评论单线程模型,因为Spring默许的便是单线程模型,而往往咱们又不指定调度线程池。所以其实单线程模型才是最最常用的。

2. @Scheduled注解解析

 经过上一章节对Spring三种守时使命类型的介绍,信任小伙伴们现已很清楚他们之间的区别了。在Spring中,守时使命都是由@Scheduled标识的,三种使命类型分别对应@Scheduled的三种特点,分别是cronfixedDelayfixedRate,设置对应的值,即为敞开对应类型的使命。

 咱们在上面也介绍过了,Spring要履行这些守时使命,榜首步便是需求先解析出来这些守时使命,然后才干交由JDK处理。那么本章节咱们就来看一下解析进程。

2.1 @EnableScheduling敞开使命调度功用

 在探求解析流程之前,咱们先介绍一下@EnableScheduling。咱们知道,在运用Spring的守时使命调度功用前,是需求在类上先增加@EnableScheduling敞开的,这究竟有什么用呢。

 关于Spring的@EnableXXX,一般届时敞开某种才能,比方EnableScheduling敞开守时使命调度、 @EnableAsync敞开异步调用等。其实原理也很简略,都是凭借@Import才能导入某些BeanPostProcessor(也有或许是其他类型的),这些BeanPostProcessor,会在bean的生命周期的各个流程发挥重要作用,从而使Spring具有强壮的才能。

 要害这个进程完全是可插拔的,加入某个BeanPostProcessor,就具有响应才能了,拓宽才能极强。这也正是@EnableXXX的原理,能够简略理解为:敞开某项功用。

BeanPostProcessor是Spring留给咱们的一种拓宽办法,才能非常强悍,甚至许多Spring的中心才能,比方@AutoWired@Resource特点注入,都是凭借它完结的。当然,@EnableXXX+@Import的组合不只仅能导入BeanPostProcessor,导入普通装备类也是能够的,并没有导入类型方面的限制。

 下面咱们看一下@EnableScheduling的做法,完结和咱们上面说的相同的,他运用@Import导入了SchedulingConfiguration这个装备类,这个装备类中运用@Bean将ScheduledAnnotationBeanPostProcessor目标放入到Spring容器中。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class) // 导入SchedulingConfiguration
@Documented
public @interface EnableScheduling {
}
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {
   //运用@Bean将ScheduledAnnotationBeanPostProcessor目标实例放入Spring容器
   @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
   @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
   public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
      return new ScheduledAnnotationBeanPostProcessor();
   }
}

聊透Spring定时任务调度

 这儿的ScheduledAnnotationBeanPostProcessor必定是Spring具有守时使命调度才能的要害。正如咱们前面提到的,它的确是承继了BeanPostProcessor,也便是大名鼎鼎的Bean的后置处理器。的确,这个bean的后置处理器,负责了@Scheduled标示的守时办法的解析、封装、调度履行等全部功用。没有它,这些作业没有人做,的确就不具有守时调度才能了。

读过贰师兄聊透Spring依靠注入一文的小伙伴,必定立刻就能想到,支撑@AutoWired特点注入的AutowiredAnnotationBeanPostProcessor,和支撑@Resource特点注入的CommonAnnotationBeanPostProcessor,不都是BeanPostProcessor嘛。

 这儿贰师兄在啰嗦一下,关于bean的后置处理器,咱们真的有必要去研究一下,其实Spring的许多强壮功用,都是依靠各个不同的BeanPostProcessor构筑出来的。他作用于bean的生命周期中的各个阶段,对bean进行功用增强,从而使bean强壮无比,去聊透Spring bean的生命周期了解一下吧,求求你们了。

2.2 创立bean时解析@Scheduled注解办法

 现在清楚了@EnableScheduling的实质是将ScheduledAnnotationBeanPostProcessor放到Spring容器中,它会负责@Scheduled标示的守时办法的解析、封装、调度履行等全部功用。那这些操作什么时分触发呢,又是怎样做的呢,是咱们接下来探求的要害。

 关于@Scheduled解析流程,贰师兄翻开ScheduledAnnotationBeanPostProcessor类,大致阅读了一下就找到是在postProcessAfterInitialization()完结的。很明显,这儿上来便是反射查找@Scheduled的办法,不是他是谁,咱们先来简略看一下源码:

// ScheduledAnnotationBeanPostProcessor.java
public Object postProcessAfterInitialization(Object bean, String beanName) {
    // 1:反射解析加了@Scheduled的办法
    Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
        (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
           Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                 method, Scheduled.class, Schedules.class);
           return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
        });
     // 2:处理加了@Scheduled的办法,(封装成调度使命)
     annotatedMethods.forEach((method, scheduledMethods) ->
           scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
     // ...省略其他代码
}
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
    // 将bean目标、办法信息封装为Runnable目标
    Runnable runnable = createRunnable(bean, method);
    // 处理cron表达式
    String cron = scheduled.cron();
    if (StringUtils.hasText(cron)) {
         // 封装成ScheduledTask,保存到tasks中
         tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
    }
    // ...省略fixedDelay和fixedRate使命的解析
}

 这儿的解析流程其实比较明晰,便是反射查找当前创立的bean,是否存在@Scheduled标示的守时使命办法。假如存在,就对守时使命办法解析、封装处理。解析无非便是对@Scheduled字段的解析;关于为什么封装,这儿需求和小伙伴解释一下,后续守时调度这些办法是经过反射的办法,反射咱们比较了解,需求method和目标信息的,而守时使命履行时刻依靠@Scheduled注解信息,所以需求将三者封装成ScheduledTask目标,先保存起来,供后续运用。

聊透Spring定时任务调度

这儿之所以先保存起来,也是Spring一向的做法都是先解析暂存,后续再运用。之所以这样是因为Spring依靠联系杂乱,容器发动进程和bean创立进程中,每个流程都做了许多工作,一般没有办法在一个机遇做完某个功用所需的所有工作,所以会分散在各个流程里,这也是Spring源码杂乱最主要的原因。

 关于@Scheduled解析机遇,解析办法都找到了,看一下调用联系就能够了,发现是在创立bean时,初始化回调时进行的,这也合理,创立bean时,解析一下bean中@Scheduled标示的守时办法。

3. @Scheduled守时使命调度履行

 现在Spring现已将加了@Scheduled的调度使命解析出来了,那下一步便是调度使命的履行了。之前咱们也说过,这部分凭借的是JDK的守时使命调度才能,Spring仅仅做了交融翻译的作业,也便是把@Scheduled标示的守时办法,翻译成契合DJK规定的守时调度使命,再交由JDK的ScheduledThreadPoolExecutor履行

 这儿能够看到,Spring所做的并不杂乱,不过仍是有一些细节需求留意,榜首个问题便是:履行使命的调度线程池从何而来。咱们知道,对JDK守时使命调度而言,调度线程池至关重要,一般榜首步便是先创立一个ScheduledThreadPoolExecutor,然后对这个调度线程池提交使命的。咱们先来看一下原生的做法:

这儿贰师兄没有自己写,而是从RocketMQ摘取了部分相关源码。rocketMQ中有很多的守时使命运用,也是运用JDK的才能,这儿咱们参阅一下:。

protected void initializeResources() {
    // 1: 创立ScheduledThreadPoolExecutor
    this.scheduledExecutorService = new ScheduledThreadPoolExecutor(1,
        new ThreadFactoryImpl("BrokerControllerScheduledThread", true, getBrokerIdentity()));
}
protected void initializeBrokerScheduledTasks() {
    // 2:提交使命到scheduledExecutorService,守时进行broker统计的使命
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            try {
                BrokerController.this.getBrokerStats().record();
            } catch (Throwable e) {
                LOG.error("BrokerController: failed to record broker stats", e);
            }
        }
    }, initialDelay, period, TimeUnit.MILLISECONDS);
}

 之所以挑选RocketMQ源码,主要是更具有代表性,免得咱们说我运用不标准。当然,别的一个原因也是贰师兄比较了解rocketMQ的源码,找起来比较快。

3.1 挑选适宜的线程池履行使命

 相关于原生运用的直接创立调度线程池,Spring会有一些小小的麻烦,那便是挑选适宜的调度线程池。假如用户指定了调度线程池还好,假如没有指定呢,否则履行了,仍是直接创立一个默许的,假如运用默许兜底,那线程数设置多少适宜呢,究竟线程数的设置要参阅:使命数和履行频率啊,这两个值每个项目又都不相同。

这儿贰师兄需求重点着重一下,给守时使命设置适宜的线程池非常重要,榜首章节就剖析过了,线程池设置的过小,会导致有些调度使命久久不能履行,从而影响数据的准确性,这一点小伙伴们必定要特别留意。

 这儿Spring的做法是先看用户是否装备了调度线程池,假如装备了,运用用户装备的;假如没有装备,创立一个默许的,可是,Spring创立的默许调度线程池,是单线程的,是单线程,是单线程!!!,重要的工作说三遍,贰师兄就在这个上面吃过亏,咱们一会看一下详细场景,加深一下小伙伴们的印象。

3.1.1 Spring查找调度线程池

 咱们先来看一下Spring挑选调度线程池的逻辑,逻辑咱们现已说清楚了,直接在源码中验证一下。

private void finishRegistration() {
      try {
         // 2.1: 获取容器中装备的TaskScheduler,没有或存在多个,都会抛出反常
         this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
      }
      catch (NoUniqueBeanDefinitionException ex) {
         try {
            //2.2 存在多个的话,再经过名称确认一个
            this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
         }
      }
      catch (NoSuchBeanDefinitionException ex) {
         try {
            // 2.3: 不存在TaskScheduler类型,获取ScheduledExecutorService类型
            this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
         }
         catch (NoUniqueBeanDefinitionException ex2) {
            try {
               // 2.4: 获取多个ScheduledExecutorService,经过名字确认一个
               this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
            }
            catch (NoSuchBeanDefinitionException ex3) {
            // 2.5: 没有打印日志即可
         }
         catch (NoSuchBeanDefinitionException ex2) {
            // 2.5: 没有打印日志即可
      }
   }
   // 调度使命履行,假如容器中不存在调度线程池,会创立默许线程池
   this.registrar.afterPropertiesSet();
}

  这部分的源码比较简略,经过resolveSchedulerBean()去Spring容器查找特定类型的bean,查找不到会抛出NoSuchBeanDefinitionException反常,查找到多个会抛出NoUniqueBeanDefinitionException反常,这儿都进行了反常捕捉,再次处理对应的逻辑。

  依照源码的逻辑,那便是:先查找TaskScheduler类型的bean,假如不存在该类型的bean,再次尝试查找ScheduledExecutorService类型的bean,真实查找不到,也便是打印一下了一下日志,而且仍是debug等级的。找到多个再次依据名称过滤一下,选定一个。

聊透Spring定时任务调度

这儿能够看到,运用用户指定的调度线程池,是看容器中有没有,所以想要指定,直接将想要运用的调度线程池放入Spring容器即可。

还有一点需求解释一下,这一步并没有构建默许的线程池,构建默许线程池的流程在下一步哦。

3.1.2 Spring构建的默许调度线程池

  在Spring容器中无法找到调度线程池时,Spring会创立默许的调度线程池,咱们也看一下这部分的逻辑。

protected void scheduleTasks() {
   if (this.taskScheduler == null) {
      // 重点:没有设置taskScheduler,默许才用单线程
      this.localExecutor = Executors.newSingleThreadScheduledExecutor();
      this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
   }
}
// 构建默许单线程的调度线程池
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}
// 构建corePoolSize为1的调度线程池
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

聊透Spring定时任务调度

3.1.3 运用默许单线程的调度线程池,导致心跳丢掉的事例剖析

这儿简略介绍一下事务场景,项目有两个守时使命,一个是核算使命,守时凌晨2点履行,履行大约时刻5分钟;还有一个心跳发送的守时使命,每10s履行一下上报,服务端有检测逻辑,假如实例超过30s未上报心跳,进行实例摘除。其时的场景便是:项目没有设置调度线程池,故主动运用了Spring默许的单线程调度线程池。

  这儿咱们用代码简略模仿一下:

@Component
public class ScheduledJob {
   /** 核算使命,凌晨2点履行,耗时五分钟 */
   @Scheduled(cron = "0 0 2 * * ?")
   void calculation() throws InterruptedException {
      System.out.println("使命1,"+Thread.currentThread().getName()+"开端履行:"+new Date());
      Thread.sleep(5 * 60 * 1000);
      System.out.println("使命1,"+Thread.currentThread().getName()+"履行完毕:"+new Date());
   }
   /** 心跳使命,每10s上报一次,耗时1s */
   @Scheduled(cron = "*/5 * * * * ?")
   void heartbeat() throws InterruptedException {
      System.out.println("使命2,"+Thread.currentThread().getName()+"开端履行:"+new Date());
      Thread.sleep(1000);
      System.out.println("使命2,"+Thread.currentThread().getName()+"履行完毕:"+new Date());
   }
}

 代码如上所示,后来项目发现:每天夜里2:00-2:05期间,实例没有心跳上报了,时刻演远远超过30s,最终导致实例被摘除,从而导致其他一系列问题。后来排查下来,便是因为没有装备调度线程池,运用了Spring默许单线程调度线程池导致的。咱们剖析一下其时的场景:

  1. 在2:00前,只有心跳使命履行,因为履行时刻短,使命不会阻塞,每次心跳都能正常上报。
  2. 在2:00时左右,核算使命发动,仅有的线程资源被占用,履行需求持续五分钟,仅有的履行线程资源在2:05才干被开释。
  3. 2:00:10 心跳使命应该被履行,可是因为没有能够线程,使命只能被抛弃。直到2:05前,都是如此,导致近五分钟不能上报心跳
  4. 2:05时左右,核算使命完毕,心跳使命才有资源履行,持续上报心跳。

 上述事例便是调度线程池设置的不合理,导致实例摘除的真实状况。经过这个事例,希望加深小伙伴们对设置适宜的调度线程池有多么重要的深入意识。赶紧检查一下自己项目调度线程池的设置吧,尤其是连设置都没有设置的小伙伴,更要当心了。

3.1.4 装备适宜的调度线程池

  现在,咱们现已知道装备适宜的调度线程池有多么的重要。关于怎么创立,连Spring怎么查找的底裤都被咱们扒出来了,怎么装备还不是小菜一碟。为了完整性,咱们仍是给咱们展示一下吧。

@EnableScheduling
@Configuration
public class ScheduleConfig {
   @Bean("threadPoolTaskScheduler")
   public TaskScheduler threadPoolTaskScheduler(){
      ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
      scheduler.setPoolSize(10);
      return scheduler;
   }
}

  是不是很简略,直接放入Spring容器中就能够了。关于你是想用@Bean、仍是想用@Component、或许想用@Import亦或是自界说BeanPostProcessor这些奇技淫巧,那就随你便了。

3.2 守时使命调度履行

  现在守时使命也查找出来了,调度线程池也有了,可谓是万事俱备,只欠东风了。咱们也总算来到使命调度履行得最终一关了。关于调度履行,当然是先把之前解析封装出来ScheduledTask使命取出来,转换翻译一下,交给JDK的调度线程池了。不过三种使命类型还不完全相同,咱们逐个来看一下。

聊透Spring定时任务调度

3.2.1 cron表达式类型的使命履行

  其实JDK的ScheduledThreadPoolExecutor自身是不支撑cron表达式类型的,这部分才能是Spring赋予的,当然,底层凭借的是ScheduledThreadPoolExecutor#schedule()单次使命调度,spring仅仅玩了一些小花样,从而使其具有了cron能够重复履行的才能。

  这儿的详细完结是:Spring先进行cron表达式的解析,核算出下一次使命的详细履行时刻,然后交由ScheduledThreadPoolExecutor#schedule()进行下次调度。不过这仍是单次的啊,不具有重复履行的才能啊,这儿Spring的小把戏就来了,在履行时刻到,ScheduledThreadPoolExecutor履行了先前提交的使命后,会再次核算出下次使命的履行时刻,再次提交给ScheduledThreadPoolExecutor。哦,原来是经过本次履行后,会提交下次使命的办法,使其具有了CRON重复履行的才能,不得不说,Spring很聪明。

  咱们直接看一下源码:

public ScheduledTask scheduleCronTask(CronTask task) {
   // 重点:2:使命调度履行阶段,将使命提交给调度线程池
   if (this.taskScheduler != null) {
      scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
   }
   // 1:@Schedule解析机遇,taskScheduler为null,仅仅仅仅将使命包装保存起来即可
   else {
      addCronTask(task);
      this.unresolvedTasks.put(task, scheduledTask);
   }
}
// # ConcurrentTaskScheduler.java
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
    //封装ReschedulingRunnable,并调度使命
    return new ReschedulingRunnable(task, trigger, this.scheduledExecutor, errorHandler).schedule();
}

  咱们发现,在使命履行的时分,先将使命封装成了ReschedulingRunnable,然后调用schedule()进行调用,貌似离中心秘密不远了,咱们持续盯梢一下。

public ScheduledFuture<?> schedule() {
   synchronized (this.triggerContextMonitor) {
      // 1:依据cron表达式,核算下次履行时刻
      this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
      if (this.scheduledExecutionTime == null) {
         return null;
      }
      //2:核算下次履行还有多少时刻
      long initialDelay = this.scheduledExecutionTime.getTime() - System.currentTimeMillis();
      //3: 将自己作为使命提交给调度线程池履行。
      this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
      return this;
   }
}

  这儿总算发现咱们想要的当地了,榜首步就解析cron表达式,核算出使命履行时刻,然后交给ScheduledThreadPoolExecutor#schedule()履行,这儿是榜首次履行使命。

关于ScheduledThreadPoolExecutor的调度原理,实质上是将守时使命,依照履行时刻,有序的保护在内部队列里,然后循环从队列获取履行时刻契合的使命,交由线程池履行。仅仅在正常线程池的使命履行的基础上,引入了时刻的概念,感兴趣的小伙伴能够自行查阅材料了解一下。

  别的,关于把this传递给schedule()去履行,或许也有点懵,什么鬼,把自己提交给调度线程池了,这啥啊。小伙伴们冷静想一想,调度线程池实质是个线程池,依据JAVA标准,咱们向线程池提交的什么,是不是Runnable实例,然后线程池履行的是Runnable#run()

  那巧了,ReschedulingRunnable就完结了Runnable,所以,把自己提交曩昔,届时分履行的便是ReschedulingRunnable#run()。这就需求咱们去看一下run()的详细完结,看一下完结逻辑是不是咱们之前剖析的:先反射履行@Schedule标示的守时办法,然后再提交CRON表达式对应的下一次使命

public void run() {
   Date actualExecutionTime = new Date();
   //1: 履行咱们界说的@Schedule办法
   super.run();
   Date completionTime = new Date();
   synchronized (this.triggerContextMonitor) {
      Assert.state(this.scheduledExecutionTime != null, "No scheduled execution");
      //2: 更新履行时刻信息
      this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
      if (!obtainCurrentFuture().isCancelled()) {
         //3:再次调用schedule办法,提交下一次使命
         schedule();
      }
   }
}

  果然不出所料,便是履行了咱们界说的@Schedule守时办法,然后提交一下车使命,不过还有一些其他的作业,比方记载一下履行时刻信息,目的是:方便进行下一次履行时刻的核算。

 这儿就从源码的视点,带咱们窥探了Spring CRON表达式完结的奥妙,总结起来便是:仍是凭借ScheduledThreadPoolExecutor#schedule()完结的,关于其不支撑的循环履行的问题,Spring采用了履行完一次使命后,回调schedule(),核算下一次履行时刻,重新提交新的使命的办法,使其具有了循环调用的逻辑。

这儿有小伙伴关于super.run()是调用咱们界说的@Schedule守时办法有所不解,这儿之所以能够这样调用,是因为在使命解析时,现已将反射需求的method信息和目标信息封装成ScheduledMethodRunnable了,其对应的run()便是反射履行该办法。在使命解析时,保存在了CronTask中。

再创立ReschedulingRunnable时,又把ScheduledMethodRunnable传递了过来,最终在父类DelegatingErrorHandlingRunnable的run()调用了ScheduledMethodRunnable的run(),所以这儿的super.run(),其实便是ScheduledMethodRunnable#run(),也便是反射履行@Schedule标示的守时办法。

 咱们总结一下cron使命的调度流程:

聊透Spring定时任务调度

3.2.2 FixedDelay使命履行

 关于FixedDelay使命的履行,则是直接凭借ScheduledThreadPoolExecutor#scheduleWithFixedDelay()完结的,这儿咱们简略看一下:

public ScheduledTask scheduleFixedDelayTask(IntervalTask task) {
   // 转换使命类型为FixedDelayTask
   FixedDelayTask taskToUse = (task instanceof FixedDelayTask ? (FixedDelayTask) task :
         new FixedDelayTask(task.getRunnable(), task.getInterval(), task.getInitialDelay()));
   return scheduleFixedDelayTask(taskToUse);
}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {
    long initialDelay = startTime.getTime() - System.currentTimeMillis();
    //直接运用ScheduledThreadPoolExecutor#scheduleWithFixedDelay()履行,
    // 可是先构建提交的Runnable目标,构建的DelegatingErrorHandlingRunnable类型
    return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), initialDelay, delay, TimeUnit.MILLISECONDS);
    // ...省略非中心代码
}

 这儿经过源码能够很清楚的看到,在核算了相关参数后,也是先构建了使命提交需求的Runnable目标,然后就直接交由ScheduledThreadPoolExecutor#scheduleWithFixedDelay()调度履行了。

 这儿咱们就不看构建的DelegatingErrorHandlingRunnable的run()办法了,这儿也是直接反射履行@Schedule标示的守时办法,并没有在做其他的工作。关于依据距离循环调度履行,ScheduledThreadPoolExecutor自身就支撑啊,这儿是不需求再做什么的。

聊透Spring定时任务调度

3.2.3 FixedRate使命履行

 关于FixedRate的履行,和FixedDelay完全相同,都是凭借ScheduledThreadPoolExecutor自身的才能完结的,这儿仅仅做转交罢了,仅仅FixedRate调用的是scheduleWithFixedRate()罢了。

聊透Spring定时任务调度

别的关于FixedRate和FixedDelay,在单线程模型下,使命履行时刻过长,关于下次使命履行时刻的影响,自身也是JDK的才能和逻辑,和Spring自身无关哦。

聊透Spring定时任务调度

3.3 守时使命触发履行的机遇

 现在关于守时使命调度履行的细节,都现已说清楚了。还有一个问题在给小伙伴弥补下,便是这些守时使命是什么时分开端调度的。

 其实这个问题不评论也完全无伤大雅,不过贰师兄之前在运用一向自研的、深度交融Spring的东西时,在碰到守时调度使命时,遇到了问题,后来排查下来发现:守时使命触发机遇,和自研东西初始化机遇重合,导致在守时使命中运用自研东西,呈现了问题。所以这儿和咱们简略介绍一下:

&emsp,首要咱们得先知道,Spring守时使命调度履行,是在接纳到ContextRefreshedEvent工作后。而自研东西也是接纳到这个工作后做中心类创立,然后注入Spring容器中的。

 因为是同一机遇触发的,先后完结状况无法保证,呈现了调度使命现已开端履行,可是中心东西类初始化还没有完结的状况,从而呈现了,在调度使命中,运用自研东西呈现空指针的状况。故这儿和咱们简略介绍一下,给咱们避个坑。

// 接纳ContextRefreshedEvent工作,调度守时使命
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
   if (event.getApplicationContext() == this.applicationContext) {
      // 查找调度线程池,提交调度使命
      finishRegistration();
   }
}

聊透Spring定时任务调度

这儿需求留意,从代码上看,在所有bean实例化后回调阶段(也便是afterSingletonsInstantiated()被调用时),也有或许会触发守时调度使命的履行,因为代码中也会调用finishRegistration()

不过机遇剖析下来,却没有在这个机遇调用,因为此刻applicationContext现已有值了,这就涉及到ApplicationContextAware的回调机遇了,贰师兄在聊透Spring bean的生命周期文章有介绍,小伙伴们依据这篇文章,自行剖析一下吧,信任必定会有所收成的,这儿不再赘述了。