This article will give you a thorough understanding of Spring's scheduled task scheduling

In this article, we talk about Spring scheduled task scheduling. I believe everyone is familiar with this topic and probably uses it more often. However, when it comes to the implementation principle, not many friends know about it. Although there are many corresponding articles on the Internet, they are basically usage tutorials. Even if there are a few mentions of principles, they are vague or even nonsense.

 In fact, Spring's scheduled task scheduling is not mysterious, nor is it unique to Spring. It is essentially implemented with the help of JDK capabilities, but it is more Spring-like in terms of usage , which is simpler and more convenient.

Regarding the JDK's scheduled tasks, it is mainly ScheduledThreadPoolExecutorimplemented with help. Interested friends can learn about its implementation principles by themselves. Here we mainly talk about Spring-related details, and we will not go into detail about the JDK part.

Of course, if you need it and I happen to have time, I can also talk about it. After all, it’s not just talking, right?

1. Types of Spring scheduled tasks

 Regarding the use of Spring scheduled tasks, everyone should be familiar with it. Just add @Scheduledannotations directly to the method. Spring will schedule the execution of this method regularly according to the frequency you specify, so you don't need to worry about this at all.

 It is naturally very simple to use, which is Spring's consistent style. Regarding how Spring does it, friends who have read Brother 2’s previous article should also be able to guess that you must at least @Scheduledfind out these annotated timing methods first, and then think of ways to make them execute regularly. Of course, as we said before, this part is achieved with the help of JDK capabilities.

 However, in the process of using Spring scheduled scheduling, there are also some details that need to be explained clearly to friends first. The first is the execution logic of the three types of tasks supported by Spring. I’m afraid not many friends can explain it clearly here, especially when encountering the single-threaded model. Let’s sort it out first:

Spring supports three task types: CRON expression type, fixedDelay interval execution, and fixedRate interval execution. Regarding the differences in task execution between these three types, we will introduce them one by one.

1.1 CRON expression type tasks

 Regarding the meaning of CRON expression, we will not introduce it here. I believe everyone is familiar with it. If you are not familiar with it, you can check the information by yourself.

 What we want to say is that in the case of single-threaded execution,如果CRON任务执行时间过长,以至于下次执行的时间都到了,但是上次任务还没有执行结束,下次任务要怎么办。

 Here is the conclusion first: 放弃, that is 下一次任务执行就被放弃了,也就是少执行了一次. Here is an expression that sets the task to be executed every five seconds to explain:

  1. Assume that at 10:00:00s, the task is executed for the first time, but the task execution time is very long, executing for 7s.
  2. According to the task execution plan, the second task should be executed at 10:00:05s, but at this time it is found that a task is being executed (the previous task needs to be executed until 10:00:07s), then, 此次执行计划直接放弃,也就是本次任务不执行了.
  3. According to the task execution plan, the third task should be executed at 10:00:10s. At this time, it was found that no task was executed, and this task was executed normally.

 Everyone must pay attention here. Under the single-threaded model, due to the long execution time of the first task, the second task is not executed, that is, it is executed once less. This is where expectations may be affected and therefore have business implications.

1.2 fixedDelay interval type tasks

 fixedDelay is the simplest method model, interval execution: that is, after delaying the specified interval, execute the next task again. The calculation formula is: 下次执行时间 = 上次任务执行结束时间 + 间隔时间. Same problem: if a task takes a long time to execute, the next execution will be later than expected. Here, the task interval is five seconds to explain:

  1. Assume that at 10:00:00s, the task is executed for the first time, and the task execution time is long, 7s.
  2. After the first task is executed (10:00:07), wait for 5 seconds and then execute the next task again (10:00:12). The second task is executed for 3 seconds.
  3. After the second task execution ends (10:00:15), wait for 5 seconds before executing the next task again, and so on.

 It should be noted here that under the single-threaded model, if there is a task that takes a long time to execute, 整体的执行计划都会往后顺延.

1.3 fixedRate interval type tasks

 FixedRate is also an interval execution method, but this interval is not calculated based on the task end time, but based on the start time. The calculation formula is: 下次执行时间 = 上次任务执行开始时间 + 间隔时间. Of course, if the task execution time is long and exceeds the interval, the next execution time will also be postponed. After all, it cannot be interrupted directly.

 However, fixedRate will automatically reduce the interval and try to catch up with the planned execution time. Once it catches up or equals, it will continue to execute at the specified interval. Here again, the interval is five seconds, and the explanation is as follows:

  1. Assume that at 10:00:00s, the task is executed for the first time, and the task execution time is long, 7s.
  2. According to the task execution plan, the second task should be executed at 10:00:05s, but the first task is still being executed at this time, so the second execution time can only be postponed.
  3. After the first task execution ended (10:00:07), it was found that it was later than the planned time of the second execution. The progress will be caught up, so the second task will be executed immediately.
  4. It is assumed here that the second task only needs to be executed for 2 seconds and ends at 10:00:09. The third execution time is planned to be: 10:00:10, which means that the second task has tied up, and there is no need to continue to catch up. At this time, the plan will be followed, and the third task will be executed normally at 10:00:10.

 Need to pay attention here, fixedRate会自动调整间隔,使任务尽快追平计划时间,追平后遵循计划执行. Of course, what is discussed here is also under the single-threaded model.

 Okay, so much discussion about the three types of scheduled tasks. Please note that the above discussion only makes sense under the single-threaded model. Everyone knows that for different task types, if the task execution time is too long, it will affect the next execution time. Again, it is emphasized that this is a single-threaded model. If it is executed by multiple threads, the impact needs to be analyzed in conjunction with the thread pool configuration. We are not qualified to discuss it here.

Why do we insist on discussing the single-threaded model here? Because Spring defaults to a single-threaded model, and often we do not specify a scheduling thread pool. So in fact, the single-threaded model is the most commonly used.

2. @Scheduled annotation analysis

 Through the introduction of Spring's three scheduled task types in the previous chapter, I believe that friends are already very clear about the differences between them. In Spring, scheduled tasks are all identified by @Scheduled. The three task types correspond to the three attributes of @Scheduled, namely, , cron. fixedDelaySetting fixedRatethe corresponding value means starting the task of the corresponding type.

 We have also introduced above that the first step for Spring to execute these scheduled tasks is to parse these scheduled tasks before they can be handed over to the JDK for processing. So in this chapter we will take a look at the parsing process.

2.1 @EnableScheduling turns on task scheduling function

 Before exploring the parsing process, let’s introduce it @EnableScheduling. As we all know, before using Spring's scheduled task scheduling function, you need to add @EnableScheduling to the class to enable it. What is the use of this?

 Regarding Spring @EnableXXX, certain capabilities are usually enabled at that time, such as EnableScheduling to enable scheduled task scheduling, @EnableAsync to enable asynchronous calls, etc. In fact, the principle is also very simple. They all use @Importcapabilities to import certain BeanPostProcessor(possibly other types). These BeanPostProcessors will play an important role in various processes of the bean life cycle, thus giving Spring powerful capabilities.

 The key point is that this process is completely pluggable. If you add a BeanPostProcessor, it will be responsive and have strong expansion capabilities. This is exactly @EnableXXXthe principle, which can be simply understood as: turning on a certain function.

BeanPostProcessorIt is an expansion method left to us by Spring. It is very powerful. Even many of Spring's core capabilities, such as @AutoWiredproperty @Resourceinjection, are realized with the help of it. Of course, the @EnableXXX+ @Importcombination can not only import BeanPostProcessor, but also ordinary configuration classes, and there are no restrictions on import types.

 Let's take a look at @EnableScheduling's approach. The implementation is the same as what we said above. It imports @Importthis SchedulingConfigurationconfiguration class. In this configuration class, @Bean is used to ScheduledAnnotationBeanPostProcessorput the object into the Spring container.

@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();
   }
}
复制代码

 This ScheduledAnnotationBeanPostProcessormust be the key to Spring's scheduled task scheduling capabilities. As we mentioned earlier, it does inherit BeanPostProcessorthe post-processor of the famous Bean. Indeed, the post-processor of this bean is responsible for all functions such as parsing, encapsulation, and scheduling execution of the timing method annotated with @Scheduled. Without it, no one would do the work, and indeed there would be no scheduled scheduling capabilities.

Friends who have read the article "Brother 2 talks about Spring dependency injection " will immediately think that those who support @AutoWiredattribute injection AutowiredAnnotationBeanPostProcessorand those who support @Resourceattribute injection CommonAnnotationBeanPostProcessorare not BeanPostProcessorthe same.

 Brother 2 would like to elaborate here. Regarding the bean post-processor, it is really necessary for everyone to study it. In fact, many of Spring's powerful functions rely on different BeanPostProcessorconstructions. It acts on each stage of the bean's life cycle and enhances the functionality of the bean, thereby making the bean extremely powerful. Let's talk about the life cycle of the Spring bean and understand it. I beg you.

2.2 Parse the @Scheduled annotation method when creating a bean

 Now it is clear @EnableSchedulingthat the essence is to ScheduledAnnotationBeanPostProcessorbe placed in the Spring container, which will be responsible for all functions such as parsing, encapsulation, and scheduling execution of the @Scheduled annotated timing method. When are these operations triggered and how are they done? This is the key to our next exploration.

 Regarding the @Scheduled parsing process, Brother 2 opened ScheduledAnnotationBeanPostProcessorthe class, browsed it briefly and found that it was being postProcessAfterInitialization()completed. Obviously, what comes up here is the method of searching @Scheduled by reflection, not who it is. Let’s take a brief look at the source code first:

// 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任务的解析
}
复制代码

 The analysis process here is actually relatively clear, that is 反射查找当前创建的bean,是否存在@Scheduled标注的定时任务方法. If it exists, parse and encapsulate the scheduled task method. Parsing is nothing more than parsing the @Scheduled field; as for why it is encapsulated, I need to explain it to my friends here. These subsequent scheduled scheduling methods are through reflection. We know better about reflection, which requires method and object information, and scheduled task execution time It relies on the @Scheduled annotation information, so the three need to be encapsulated into ScheduledTaskobjects and saved first for subsequent use.

The reason why it is saved here first is because Spring's consistent practice is to parse the temporary storage first and then use it later. The reason for this is that Spring's dependencies are complex. During the container startup process and the bean creation process, each process does a lot of things. Usually there is no way to complete all the things required for a certain function at one time, so it will be scattered across various processes. In the process, this is also the main reason why Spring source code is complicated.

 Regarding the timing of @Scheduled parsing, the parsing methods have been found. Just look at the calling relationship. It is found that it is done during the initialization callback when the bean is created. This is also reasonable. When creating the bean, parse the timing of the @Scheduled annotation in the bean. method.

3. @Scheduled scheduled task scheduling and execution

 Now that Spring has parsed the scheduled tasks with @Scheduled added, the next step is to execute the scheduled tasks. We have also said before that this part relies on the JDK's scheduled task scheduling capabilities, and Spring only does the work of integrating translation, that is 把@Scheduled标注的定时方法,翻译成符合DJK规定的定时调度任务,再交由JDK的ScheduledThreadPoolExecutor执行.

 As you can see here, what Spring does is not complicated, but there are still some details that need to be paid attention to. The first question is: 执行任务的调度线程池从何而来. We know that for JDK scheduled task scheduling, the scheduling thread pool is crucial. Usually the first step is to create one ScheduledThreadPoolExecutorand then submit tasks to this scheduling thread pool. Let’s take a look at the native approach first:

Brother 2 did not write it himself here, but extracted some relevant source code from RocketMQ. There are a large number of scheduled tasks used in rocketMQ, which also use the ability of JDK. Here we refer to:.

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);
}
复制代码

 The reason why I chose RocketMQ source code is mainly because it is more representative, so as to avoid people saying that I use it irregularly. Of course, another reason is that Brother 2 is more familiar with the source code of rocketMQ and can find it faster.

3.1 Choose an appropriate thread pool to perform tasks

 Compared with the native use of directly creating a scheduling thread pool, Spring will have some minor troubles, that is 选择合适的调度线程池. It's okay if the user specifies the scheduling thread pool. If not, otherwise it will be executed and a default one will be created directly. If the default is used, what is the appropriate number of threads to set? After all, the setting of the number of threads should refer to: the number of tasks and Execution frequency, these two values ​​​​are different for each project.

Brother 2 needs to emphasize here that it is very important to set up a suitable thread pool for scheduled tasks. As analyzed in the first chapter, if the thread pool is set too small, some scheduling tasks will not be executed for a long time, thus affecting the accuracy of the data. , friends must pay special attention to this.

 What Spring does here is to first check whether the user has configured a scheduling thread pool. If configured, use the one configured by the user; if not configured, create a default one. However, the default scheduling thread pool created by Spring, say important things three times 是单线程的,是单线程,是单线程!!!. , Senior Brother 2 has suffered a loss in this regard. We will take a look at the specific scenes in a moment to deepen the impressions of our friends.

3.1.1 Spring search scheduling thread pool

 Let's first take a look at the logic of Spring's selection of scheduling thread pools. We have already explained the logic clearly and can verify it directly in the source code.

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();
}
复制代码

  The source code of this part is relatively simple. By resolveSchedulerBean()searching for a specific type of bean in the Spring container, NoSuchBeanDefinitionExceptionan exception will be thrown if no bean is found. If more than one bean is found NoUniqueBeanDefinitionException, an exception will be thrown. The exceptions are captured here and the corresponding logic is processed again.

  According to the logic of the source code, that is: 先查找TaskScheduler类型的bean, if there is no bean of this type, 再次尝试查找ScheduledExecutorService类型的bean, and it cannot be found, that is, the log is printed, and it is at debug level. If you find multiple ones, filter them again by name and select one.

You can see here that using the user-specified scheduling thread pool depends on whether it is available in the container. So if you want to specify it, just put the scheduling thread pool you want to use into the Spring container.

There is another point that needs to be explained. This step does not build the default thread pool. The process of building the default thread pool is in the next step.

3.1.2 Default scheduling thread pool built by Spring

  When the scheduling thread pool cannot be found in the Spring container, Spring will create a default scheduling thread pool. Let's also take a look at this part of the logic.

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());
}
复制代码

3.1.3 Case analysis of heartbeat loss caused by using the default single-threaded scheduling thread pool

Here is a brief introduction to the business scenario. The project has two scheduled tasks. One is a computing task, which is scheduled to be executed at 2 a.m. and takes about 5 minutes to execute. There is also a scheduled task for heartbeat sending, which is executed and reported every 10 seconds. There is detection on the server side. Logically, if the instance does not report heartbeat for more than 30 seconds, the instance will be removed. The scenario at that time was: the project did not set up a scheduling thread pool, so Spring's default single-thread scheduling thread pool was automatically used.

  Here we simply simulate it with code:

@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());
   }
}
复制代码

 The code is as shown above. Later, the project found that between 2:00 and 2:05 every night, the instance reported no heartbeat. The time elapsed was far more than 30 seconds, which eventually led to the instance being removed, which in turn led to a series of other problems. Later, after investigation, it was found that the scheduling thread pool was not configured and Spring's default single-thread scheduling thread pool was used. Let’s analyze the scene at that time:

  1. Before 2:00, only the heartbeat task is executed. Because the execution time is short, the task will not be blocked, and each heartbeat can be reported normally.
  2. At around 2:00, the computing task starts, the only thread resource is occupied, the execution takes five minutes, and the only execution thread resource is released at 2:05.
  3. 2:00:10 Heartbeat mission 应该被执行,但是由于没有可以线程,任务只能被放弃. This was the case until 2:05, resulting in 近五分钟不能上报心跳.
  4. At around 2:05, the computing task ends, and the heartbeat task has resources to execute, and heartbeat reporting continues.

 The above case is the unreasonable setting of the scheduling thread pool, which leads to the real situation of instance removal. Through this case, I hope to deepen everyone's understanding of how important it is to set up a suitable scheduling thread pool. Hurry up and check the settings of your project scheduling thread pool, especially those who have not even set up the settings, you should be more careful.

3.1.4 Configure a suitable scheduling thread pool

  Now, we already know how important it is to configure a suitable scheduling thread pool. Regarding how to create it, we have even figured out how to find it in Spring. How to configure it is not a piece of cake. For the sake of completeness, let’s show it to you.

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

  Isn't it very simple? Just put it directly into the Spring container. It's up to you whether you want to use it @Bean, use it @Component, use it @Importor customize BeanPostProcessorit.

3.2 Scheduled task execution

  Now that the scheduled tasks have been found and the scheduling thread pool is available, everything is ready and all we need is the east wind. We have finally reached the final level of task scheduling execution. ScheduledTaskRegarding scheduling execution, of course, we first take out the previously parsed and encapsulated tasks, convert and translate them, and hand them over to the JDK's scheduling thread pool. However, the three task types are not exactly the same, so let’s look at them one by one.

3.2.1 Task execution of cron expression type

  In fact, JDK's ScheduledThreadPoolExecutor itself does not support the cron expression type. This part of the ability is given by Spring. Of course, the bottom layer relies on ScheduledThreadPoolExecutor#schedule() single task scheduling. Spring just plays some tricks to make it have Cron has the ability to execute repeatedly.

  The specific implementation here is: Spring first parses the cron expression, calculates the specific execution time of the next task, and then hands it over to ScheduledThreadPoolExecutor#schedule() for the next scheduling. However, this is still a single time and does not have the ability to be executed repeatedly. Here comes Spring's little trick. When the execution time is up, after the ScheduledThreadPoolExecutor executes the previously submitted task, it will calculate the execution time of the next task again. Submit to ScheduledThreadPoolExecutor. Oh, it turns out 本次执行后,会提交下次任务的方式that it has the ability to execute CRON repeatedly. I have to say that Spring is very smart.

  Let’s take a look directly at the source code:

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();
}
复制代码

  We found that when the task is executed, the task is encapsulated first ReschedulingRunnable, and then schedule() is called. It seems that the core secret is not far away. Let's continue to track it.

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;
   }
}
复制代码

  Here we finally find what we want. The first step is to parse the cron expression, calculate the task execution time, and then hand it over to ScheduledThreadPoolExecutor#schedule() for execution. This is the first time the task is executed.

Regarding the scheduling principle of ScheduledThreadPoolExecutor, it is essentially to maintain scheduled tasks in an internal queue in an orderly manner according to the execution time, and then obtain tasks that meet the execution time from the queue in a loop and hand them over to the thread pool for execution. Only on the basis of normal thread pool task execution, the concept of time is introduced. Interested friends can check the information by themselves.

In addition, you may be a little confused   about thispassing it to execution. What the hell, you submitted yourself to the scheduling thread pool, what the hell. schedule()Friends, think about it calmly. The scheduling thread pool is essentially a thread pool. According to the JAVA specification, what we submit to the thread pool is an Runnableinstance, and then the thread pool executes it Runnable#run().

  By coincidence, ReschedulingRunnableit was realized Runnable, so submit yourself and it will be executed when the time comes ReschedulingRunnable#run(). This requires us to take a look at the specific implementation of run() and see if the implementation logic is what we analyzed before: first 反射执行@Schedule标注的定时方法, then 再提交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();
      }
   }
}
复制代码

  As expected, the @Schedule timing method we defined is executed, and then the bus task is submitted. However, there is also some other work, such as recording the execution time information, for the purpose of facilitating the calculation of the next execution time.

 Here, from the perspective of source code, we will take a peek at the secrets of Spring CRON expression implementation. To sum up, it is still implemented with the help of ScheduledThreadPoolExecutor#schedule(). For the problem of loop execution that it does not support, Spring adopts执行完一次任务后,回调schedule(),计算下一次执行时间,重新提交新的任务的方式,使其具备了循环调用的逻辑。

Some friends here are confused about super.run() calling the @Schedule timing method we defined. The reason why it can be called here is because during task parsing, the method information and object information required for reflection have been encapsulated into ScheduledMethodRunnable, which corresponds run()to reflection execution of this method. During task parsing, it is saved in CronTask.

When creating ReschedulingRunnable again, ScheduledMethodRunnable is passed over, and finally the run() of ScheduledMethodRunnable is called in the run() of the parent class DelegatingErrorHandlingRunnable, so the super.run() here is actually ScheduledMethodRunnable#run(), which is reflection execution. Timing method marked with @Schedule.

 Let’s summarize the cron task scheduling process:

3.2.2 FixedDelay task execution

 Regarding the execution of the FixedDelay task, it is ScheduledThreadPoolExecutor#scheduleWithFixedDelay()completed directly with the help of it. Here we take a brief look:

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);
    // ...省略非核心代码
}
复制代码

 It can be clearly seen from the source code here that after calculating the relevant parameters, the Runnable object required for task submission is first constructed, and then directly handed over to ScheduledThreadPoolExecutor#scheduleWithFixedDelay() for scheduling and execution.

 Here we will not look at the run() method of the built DelegatingErrorHandlingRunnable. The same is true here 直接反射执行@Schedule标注的定时方法, and it is not doing other things. Regarding scheduling execution according to the interval loop, ScheduledThreadPoolExecutor itself supports it, so there is no need to do anything else here.

3.2.3 FixedRate task execution

 Regarding the execution of FixedRate, it is exactly the same as FixedDelay. It is realized with the help of the ability of ScheduledThreadPoolExecutor itself. Here it is just a transfer, but FixedRate calls scheduleWithFixedRate().

In addition, regarding FixedRate and FixedDelay, under the single-threaded model, if the task execution time is too long, the impact on the next task execution time is itself the capability and logic of the JDK, and has nothing to do with Spring itself.

3.3 Timing of triggering execution of scheduled tasks

 Now the details of scheduled task scheduling and execution have been explained clearly. Another question I would like to add to my friends is when do these scheduled tasks start to be scheduled.

 In fact, it is completely harmless not to discuss this issue. However, when Brother 2 used self-developed tools that were deeply integrated with Spring, he encountered problems when encountering scheduled scheduling tasks. Later, after troubleshooting, he found: the timing of triggering tasks, and The initialization timing of self-developed tools coincides with each other, causing problems when using self-developed tools in scheduled tasks. So here is a brief introduction to you:

&emsp, first of all, we must know that Spring scheduled task scheduling is executed after receiving ContextRefreshedEventthe event. The self-developed tools also create core classes after receiving this event, and then inject them into the Spring container.

 Since they are triggered at the same time, the completion status cannot be guaranteed. The scheduling task has started to be executed, but the initialization of the core tool class has not been completed. Then, in the scheduling task, a null pointer appears when using self-developed tools. . Therefore, I will briefly introduce it to you to avoid pitfalls.

// 接收ContextRefreshedEvent事件,调度定时任务
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
   if (event.getApplicationContext() == this.applicationContext) {
      // 查找调度线程池,提交调度任务
      finishRegistration();
   }
}
复制代码

It should be noted here that from the code point of view, during the callback phase after all beans are instantiated (that is, afterSingletonsInstantiated()when they are called), the execution of scheduled scheduling tasks may also be triggered because they are also called in the code finishRegistration().

However, after analyzing the timing, it was not called at this time because it applicationContextwas already valuable at this time, which involves ApplicationContextAwarethe callback timing.

Guess you like

Origin blog.csdn.net/2301_76607156/article/details/130526168