この記事では、Spring のスケジュールされたタスクのスケジューリングについて完全に理解します。

この記事では、Spring でスケジュールされたタスクのスケジューリングについて説明します。このトピックについては誰もがよく知っており、おそらくより頻繁に使用されると思います。ただし、実装原理となると、それを知っている人は少なく、インターネット上には対応する記事がたくさんありますが、基本的には使い方のチュートリアルです。たとえ原則について少し言及されていたとしても、それらは曖昧であるか、ナンセンスですらあります。

 実際、Spring のスケジュールされたタスクのスケジューリングは不思議なものではなく、Spring に固有のものでもありません。基本的には JDK 機能を利用して実装されていますが、使用方法の点では Spring に似ており、よりシンプルで便利です

JDK のスケジュールされたタスクについては、主にScheduledThreadPoolExecutorヘルプを使用して実装されていますが、興味のある友人はその実装原理を自分で学ぶことができます。ここでは主に Spring 関連の詳細について説明し、JDK 部分については詳しく説明しません。

もちろん、必要で時間があれば、お話しすることもできます。

1. Spring スケジュールされたタスクの種類

 Spring のスケジュールされたタスクの使用については、@Scheduledメソッドに直接アノテーションを追加するだけなので、誰もがよく知っているはずです。Spring は指定した頻度に従ってこのメソッドの実行を定期的にスケジュールするため、これについてはまったく心配する必要はありません。

 当然ながら使い方は非常に簡単で、これが Spring の一貫したスタイルです。Spring がどのように行うかについては、Brother 2 の以前の記事を読んだ友人なら、少なくとも最初に@Scheduledこれらの注釈付きタイミング メソッドを見つけて、それからそれらを定期的に実行する方法を考える必要があることを推測できるはずです。もちろん、前に述べたように、この部分は JDK 機能の助けを借りて実現されます。

 ただし、春のスケジュール設定を使用する過程では、最初に友人に明確に説明する必要がある詳細もいくつかあります。1 つ目は、Spring でサポートされている 3 種類のタスクの実行ロジックです。特にシングルスレッド モデルに遭遇した場合、ここで明確に説明できる友人は多くないと思います。最初に整理しましょう:

Spring は、CRON 式タイプ、fixedDelay 間隔実行、および fixRate 間隔実行の 3 つのタスク タイプをサポートします。これら 3 種類のタスク実行の違いについて、順に紹介していきます。

1.1 CRON式タイプのタスク

 CRON 式の意味については、ここでは紹介しませんが、皆さんよくご存知かと思いますが、ご存じない方はご自身で調べていただければと思います。

 私たちが言いたいのは、シングルスレッド実行の場合、如果CRON任务执行时间过长,以至于下次执行的时间都到了,但是上次任务还没有执行结束,下次任务要怎么办。

 結論を先に言いますと、放弃、つまり です下一次任务执行就被放弃了,也就是少执行了一次説明のために、タスクを 5 秒ごとに実行するように設定する式を次に示します。

  1. 10:00:00 秒にタスクが初めて実行されるとしますが、タスクの実行時間は非常に長く、7 秒かかります。
  2. タスク実行計画によれば、2番目のタスクは10:00:05秒に実行されるはずですが、この時点でタスクが実行中であることがわかります(前のタスクは10:00:07秒まで実行する必要があります)。 、此次执行计划直接放弃,也就是本次任务不执行了
  3. タスク実行計画によれば、3番目のタスクは10:00:10秒に実行されるはずですが、この時点ではタスクが実行されていないことがわかり、このタスクは正常に実行されました。

 ここで注意していただきたいのは、シングルスレッドモデルでは、最初のタスクの実行時間が長いため、2番目のタスクは実行されず、実行回数が1回減ります。ここは期待に影響を与える可能性があり、ビジネスに影響を与える可能性があります。

1.2 fixDelay 間隔タイプのタスク

 fixDelay は最も単純なメソッド モデルであり、間隔実行です。つまり、指定された間隔を遅らせた後、次のタスクを再度実行します。計算式は次のとおりです下次执行时间 = 上次任务执行结束时间 + 间隔时间同じ問題: タスクの実行に時間がかかると、次の実行が予想より遅くなります。ここでは、タスク間隔を 5 秒として説明します。

  1. 10:00:00 秒にタスクが初めて実行され、タスクの実行時間が 7 秒と長いとします。
  2. 最初のタスクが実行された後 (10:00:07)、5 秒待ってから次のタスクが再度実行されます (10:00:12)。2 番目のタスクは 3 秒間実行されます。
  3. 2 番目のタスクの実行が終了した後 (10:00:15)、5 秒待ってから次のタスクを再度実行する、というように続きます。

 ここで注意すべき点は、シングルスレッドモデルでは、実行に長時間かかるタスクがある場合、整体的执行计划都会往后顺延.

1.3 固定レート間隔タイプのタスク

 FixedRate も間隔実行方式ですが、この間隔はタスクの終了時刻ではなく、開始時刻に基づいて計算されます。計算式は次のとおりです下次执行时间 = 上次任务执行开始时间 + 间隔时间もちろん、タスクの実行時間が長くてインターバルを超えた場合には、次の実行時間も延期されますので、やはり直接中断することはできません。

 ただし、fixedRate は自動的に間隔を減らして予定の実行時間に追いつくことを試み、追いつくか等しくなったら指定した間隔で実行を続けます。ここでも間隔は 5 秒で、説明は次のとおりです。

  1. 10:00:00 秒にタスクが初めて実行され、タスクの実行時間が 7 秒と長いとします。
  2. タスク実行計画によれば、2 番目のタスクは 10:00:05 秒に実行されるはずですが、この時点では最初のタスクがまだ実行中であるため、2 番目の実行時刻は延期することしかできません。
  3. 1回目のタスク実行終了(10:00:07)後、2回目の実行予定時刻よりも遅れていることが判明しました。進捗が追いつくため、すぐに 2 番目のタスクが実行されます。
  4. ここでは、2 番目のタスクは 2 秒間実行するだけで済み、10:00:09 に終了すると仮定します。3 番目の実行時間は 10:00:10 になるように計画されています。これは、2 番目のタスクが同点であり、追いつき続ける必要がないことを意味します。この時点では、計画に従い、3 番目のタスクは実行されます。通常は 10:00:10 に実行されます。

 ここで注意が必要ですfixedRate会自动调整间隔,使任务尽快追平计划时间,追平后遵循计划执行もちろん、ここで説明する内容もシングルスレッド モデルの下での話です。

 さて、3 種類のスケジュールされたタスクについての議論はこれくらいです。上記の説明は、シングルスレッド モデルの下でのみ意味をなすことに注意してください。さまざまなタスクの種類において、タスクの実行時間が長すぎると、次の実行時間に影響することは誰もが知っています。繰り返しになりますが、これはシングルスレッド モデルであることを強調します。複数のスレッドで実行される場合、その影響はスレッド プールの構成と併せて分析する必要があります。ここで議論する資格はありません。

なぜここでシングルスレッド モデルについて議論する必要があるのでしょうか? Spring のデフォルトはシングルスレッド モデルであり、多くの場合、スケジューリング スレッド プールを指定しないからです。したがって、実際には、シングルスレッド モデルが最も一般的に使用されます。

2. @スケジュールされたアノテーション分析

 前の章で Spring の 3 つのスケジュールされたタスク タイプを紹介したことで、友人たちはそれらの違いについてすでに十分に理解していると思います。Spring では、スケジュールされたタスクはすべて @Scheduled によって識別されます。3 つのタスク タイプは、 @Scheduled の 3 つの属性 ( 、 ) に対応します。cron対応する値を設定すると、対応するタイプのタスクが開始されますfixedDelayfixedRate

 Spring がこれらのスケジュールされたタスクを実行するための最初のステップは、処理のために JDK に引き渡される前にこれらのスケジュールされたタスクを解析することであることも上で紹介しました。したがって、この章では、解析プロセスを見ていきます。

2.1 @EnableScheduling はタスク スケジューリング機能をオンにします

 解析プロセスを説明する前に、それを紹介しましょう@EnableSchedulingご存知のとおり、Spring のスケジュールされたタスクのスケジューリング機能を使用する前に、 @EnableScheduling をクラスに追加して有効にする必要があります。

 Spring に関しては@EnableXXX、通常、スケジュールされたタスクのスケジューリングを有効にする EnableScheduling 、非同期呼び出しを有効にする @EnableAsync など、特定の機能がその時点で有効になります。実際、原理も非常に単純です。これらはすべて、@Import特定の (おそらく他のタイプ) をインポートする機能をBeanPostProcessor使用します。これらの BeanPostProcessor は、Bean ライフサイクルのさまざまなプロセスで重要な役割を果たし、Spring に強力な機能を与えます。

 重要な点は、このプロセスが完全にプラグイン可能であるということであり、BeanPostProcessor を追加すると、応答性が高く、強力な拡張機能を備えます。これはまさに@EnableXXX原理であり、単純に「特定の機能をオンにする」と理解できます。

BeanPostProcessor@AutoWiredこれは Spring が残した拡張メソッドであり、非常に強力であり、プロパティ注入などの Spring の中核機能の多くも@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();
   }
}
复制代码

 これはScheduledAnnotationBeanPostProcessorSpring のスケジュールされたタスクのスケジューリング機能の鍵となるはずです。前に述べたように、BeanPostProcessor有名な Bean のポストプロセッサを継承しています。実際、この Bean のポストプロセッサは、解析、カプセル化、@Scheduled アノテーションが付けられたタイミング メソッドの実行のスケジュール設定などのすべての機能を担当します。それがなければ誰もその作業を行うことはなく、実際にスケジュールされたスケジューリング機能も存在しません。

「兄弟 2 がSpring 依存性注入について語る」という記事を読んだ友人は、@AutoWired属性注入をサポートする人と属性注入AutowiredAnnotationBeanPostProcessorをサポートする人は同じではないとすぐに考えるでしょう。@ResourceCommonAnnotationBeanPostProcessorBeanPostProcessor

 兄 2 がここで詳しく説明したいと思います。Bean ポストプロセッサーについては、誰もがそれを勉強する必要があります。実際、Spring の強力な機能の多くはさまざまな構造に依存していますBeanPostProcessorSpring Bean のライフ サイクルの各段階で作用し、Bean の機能を強化し、それによって Bean を非常に強力なものにします。Spring Bean のライフ サイクルについて話して、それを理解しましょう。お願いします。

2.2 Bean 作成時に @Scheduled アノテーション メソッドを解析する

@EnableSchedulingこれで、エッセンスが Spring コンテナに配置されることは 明らかですScheduledAnnotationBeanPostProcessor。Spring コンテナは、解析、カプセル化、@Scheduled アノテーション付きタイミング メソッドの実行スケジュールなどのすべての機能を担当します。これらの操作はいつトリガーされ、どのように実行されるのでしょうか? これが次の探索の鍵となります。

 @Scheduled の解析プロセスに関しては、兄弟 2 が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 フィールドを解析することに他なりません。なぜカプセル化されるのかについては、ここで友人に説明する必要があります。これらの後続のスケジュールされたスケジュール方法はリフレクションを通じて行われます。リフレクションについては、メソッドとオブジェクトの情報が必要であるため、私たちはよく知っています。これは @Scheduled アノテーション情報に依存するため、これら 3 つをScheduledTaskオブジェクトにカプセル化して、後で使用するために最初に保存する必要があります。

最初にここに保存される理由は、Spring の一貫した慣行が最初に一時ストレージを解析し、後でそれを使用するためです。その理由は、Spring の依存関係が複雑であるためです。コンテナの起動プロセスと Bean の作成プロセス中に、各プロセスで多くの処理が実行されます。通常、特定の機能に必要なすべての処理を一度に完了する方法はありません。このプロセスにおいて、Spring のソース コードが複雑になる主な理由もこれです。

 @Scheduled の解析のタイミングについては、解析方法が判明しました。呼び出し関係を見ると、Bean 作成時の初期化コールバック中に実行されていることがわかります。これも合理的です。Bean 作成時に解析します。 Bean.メソッド内の @Scheduled アノテーションのタイミング。

3. @Scheduled スケジュールされたタスクのスケジュールと実行

 Spring が @Scheduled を追加してスケジュールされたタスクを解析したので、次のステップはスケジュールされたタスクを実行することです。また、この部分は JDK のスケジュールされたタスクのスケジューリング機能に依存しており、Spring は翻訳を統合する作業のみを実行することも前に述べました把@Scheduled标注的定时方法,翻译成符合DJK规定的定时调度任务,再交由JDK的ScheduledThreadPoolExecutor执行

 ここでわかるように、Spring の動作は複雑ではありませんが、注意が必要な詳細がいくつかあります执行任务的调度线程池从何而来JDK のスケジュールされたタスクのスケジューリングでは、スケジューリング スレッド プールが重要であることはわかっていますが、通常、最初の手順ではスケジューリング スレッド プールを作成し、ScheduledThreadPoolExecutorこのスケジューリング スレッド プールにタスクを送信します。まずネイティブのアプローチを見てみましょう。

Brother 2 はここに自分で書いたわけではありませんが、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 の方がより代表的であり、不規則に使用していると言われるのを避けるためです。もちろん、もう 1 つの理由は、Brother 2 の方が rocketMQ のソース コードに精通しており、より速く見つけることができるということです。

3.1 タスクを実行するために適切なスレッド プールを選択する

 スケジューリング スレッド プールを直接作成するネイティブの使用と比較すると、Spring にはいくつかの小さな問題が発生します选择合适的调度线程池ユーザーがスケジューリング スレッド プールを指定しても問題ありません。そうでない場合は、スケジューリング スレッド プールが実行され、デフォルトのプールが直接作成されます。デフォルトを使用する場合、適切なスレッド数を設定するにはどうすればよいですか? 結局のところ、スレッド数はタスクの数と実行頻度を参照する必要があります。これら 2 つの値はプロジェクトごとに異なります。

ここで兄弟 2 は、スケジュールされたタスクに適切なスレッド プールを設定することが非常に重要であることを強調する必要があります。最初の章で分析したように、スレッド プールの設定が小さすぎると、一部のスケジュール タスクが長時間実行されなくなります。したがって、データの精度に影響を与えるため、友人はこれに特別な注意を払う必要があります。

 ここで Spring が行うことは、まずユーザーがスケジューリング スレッド プールを設定したかどうかを確認することです。設定されている場合は、ユーザーが設定したものを使用します。設定されていない場合は、デフォルトのスレッド プールを作成します。ただし、Spring によって作成されるデフォルトのスケジューリング スレッド プールは、たとえば、大切なことは3回是单线程的,是单线程,是单线程!!!. , Senior Brother 2 has been a loss in this matter. では、具体的なシーンを見て、友達の印象を深めていきます。

3.1.1 Spring Searchのスケジューリングスレッドプール

 まず 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 を検索することで、NoSuchBeanDefinitionExceptionBean が見つからない場合は例外がスローされ、複数の Bean が見つかった場合はNoUniqueBeanDefinitionException例外がスローされます。ここでキャプチャされ、対応するロジックが再度処理されます。

  ソース コードのロジックによれば、つまり、先查找TaskScheduler类型的beanこのタイプの Bean がなく、再次尝试查找ScheduledExecutorService类型的bean見つからない場合は、ログが出力され、デバッグ レベルになります。複数のものが見つかった場合は、名前で再度フィルターし、1 つを選択します。

ここで、ユーザー指定のスケジューリング スレッド プールを使用するかどうかは、コンテナ内で使用できるかどうかに依存することがわかります。そのため、指定したい場合は、使用したいスケジューリング スレッド プールを Spring コンテナーに配置するだけです。

もう 1 つ説明しなければならない点がありますが、このステップではデフォルトのスレッド プールは構築されません。デフォルトのスレッド プールを構築するプロセスは次のステップで行われます。

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

3.1.3 デフォルトのシングルスレッドスケジューリングスレッドプールの使用によって引き起こされるハートビート損失のケース分析

ビジネス シナリオを簡単に紹介します。プロジェクトには 2 つのスケジュールされたタスクがあります。1 つはコンピューティング タスクで、午前 2 時に実行されるようにスケジュールされており、実行には約 5 分かかります。また、ハートビート送信のスケジュールされたタスクもあります。 10 秒ごとに実行され、報告されます。サーバー側で検出があります。論理的に、インスタンスが 30 秒を超えてハートビートを報告しない場合、インスタンスは削除されます。その時のシナリオは次のとおりでした。プロジェクトはスケジューリング スレッド プールをセットアップしなかったため、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 の間、インスタンスがハートビートを報告していないことを発見しました。経過時間は 30 秒をはるかに超えており、最終的にインスタンスは削除されました。その結果、他の一連の問題が発生しました。その後の調査により、スケジューリング スレッド プールが設定されておらず、Spring のデフォルトのシングルスレッド スケジューリング スレッド プールが使用されていることが判明しました。当時の場面を分析してみましょう。

  1. 2:00以前はハートビートタスクのみ実行されますが、実行時間が短いためタスクがブロックされず、各ハートビートが正常に報告されます。
  2. 2:00 頃に計算タスクが開始され、唯一のスレッド リソースが占有され、実行には 5 分かかり、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 のスケジューリング スレッド プールに渡します。ただし、3 つのタスク タイプはまったく同じではないため、1 つずつ見てみましょう。

3.2.1 cron式タイプのタスク実行

  実際, JDK の ScheduledThreadPoolExecutor 自体は cron 式タイプをサポートしていません. この機能のこの部分は Spring によって提供されます. もちろん, 最下層は ScheduledThreadPoolExecutor#schedule() 単一タスクのスケジューリングに依存しています. Spring はそれを実現するためにいくつかのトリックを実行しているだけですCron には繰り返し実行する機能があります。

  ここでの具体的な実装は次のとおりです。 Spring はまず cron 式を解析し、次のタスクの特定の実行時間を計算し、それを次のスケジューリングのために ScheduledThreadPoolExecutor#schedule() に渡します。ただし、これはまだ 1 回限りであり、繰り返し実行する機能はありません。ここに 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() の特定の実装を調べて、実装ロジックが以前に分析したものであるかどうかを確認する必要があります ( 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();
      }
   }
}
复制代码

  予想どおり、定義した @Schedule タイミング メソッドが実行され、バス タスクがサブミットされますが、次回の実行時間の計算を容易にするために、実行時間情報を記録するなど、他の作業もいくつかあります。 。

 ここでは、ソースコードの観点から Spring CRON 式の実装の秘密を覗いてみましょう. 要約すると、これは依然として ScheduledThreadPoolExecutor#schedule() の助けを借りて実装されています. ループ実行の問題については、サポートされていない、Spring が採用执行完一次任务后,回调schedule(),计算下一次执行时间,重新提交新的任务的方式,使其具备了循环调用的逻辑。

ScheduledMethodRunnableここの友人の中には、私たちが定義した @Schedule タイミング メソッドを呼び出す super.run() について混乱している人もいますが、ここで呼び出すことができる理由は、タスクの解析中に、リフレクションに必要なメソッド情報とオブジェクト情報が にカプセル化されているためです。run()このメソッドの実行を反映します。タスクの解析中に、タスクは CronTask に保存されます。

再度 ReschedulingRunnable を作成すると、ScheduledMethodRunnable が渡され、最終的に親クラス DelegatingErrorHandlingRunnable の run() 内で ScheduledMethodRunnable の run() が呼び出されるため、ここでの super.run() は実際には ScheduledMethodRunnable#run() となり、これがリフレクションになります。 @Schedule でマークされたタイミング メソッド。

 cron タスクのスケジュール設定プロセスを要約してみましょう。

3.2.2FixedDelayタスクの実行

 FixedDelay タスクの実行に関しては、FixedDelayScheduledThreadPoolExecutor#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 自体がサポートしているため、ここでは特に何もする必要はありません。

3.2.3 固定レートタスクの実行

 FixedRateの実行に関してはFixedDelayと全く同じで、ScheduledThreadPoolExecutor自体の能力を利用して実現されており、ここでは単なる転送ですが、FixedRateはscheduleWithFixedRate()を呼び出します。

なお、FixedRateとFixedDelayに関しては、シングルスレッドモデルにおいて、タスクの実行時間が長すぎる場合、次のタスクの実行時間に与える影響自体はJDKの能力やロジックであり、Spring本体とは関係ありません。 。

3.3 スケジュールされたタスクの実行をトリガーするタイミング

 スケジュールされたタスクのスケジューリングと実行の詳細が明確に説明されました。友達に追加したいもう 1 つの質問は、これらのスケジュールされたタスクはいつからスケジュールされ始めるのかということです。

 実際、この問題について議論しないのはまったく無害です。ただし、Brother 2 が Spring と深く統合された自社開発ツールを使用したとき、スケジュールされたスケジューリング タスクに遭遇したときに問題に遭遇しました。その後、トラブルシューティングを行った後、彼は次のことを発見しました。タスクのトリガーと、自社開発ツールの初期化タイミングが重なるため、スケジュールされたタスクで自社開発ツールを使用する場合に問題が発生します。ここで簡単に紹介します。

ContextRefreshedEvent&emsp、まず第一に、Spring のスケジュールされたタスクのスケジューリングがイベントの受信後に実行されることを知っておく必要があります。自社開発ツールもこのイベントを受け取った後にコアクラスを作成し、Springコンテナに注入します。

 同時にトリガーされるため、完了状態は保証できません スケジューリングタスクの実行は開始されていますが、コアツールクラスの初期化が完了していない場合、スケジューリングタスク内でヌルポインタが表示される独自に開発したツールを使用します。そこで、落とし穴を避けるために簡単に紹介します。

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

ここで、コードの観点から見ると、すべての Bean がインスタンス化された後 (つまり、Bean がafterSingletonsInstantiated()呼び出されたとき) のコールバック フェーズで、スケジュールされたスケジューリング タスクの実行もトリガーされる可能性があることに注意してください。これは、これらのタスクもコード内で呼び出されるためです。finishRegistration()

しかし、タイミングを分析したところ、コールバックのタイミングがapplicationContext関係するこの時点で既に価値があったため、この時点では呼び出されませんでした。ApplicationContextAware

おすすめ

転載: blog.csdn.net/2301_76607156/article/details/130526168