Spring Bootの @Scheduled を使った定期実行処理の作り方

Java


スポンサーリンク

✅ はじめに

Spring Bootで「定期的な処理」を実行したいとき、最もシンプルに実装できる方法が @Scheduled アノテーションです。

このガイドでは、

  • @Scheduled の使い方
  • cron式やdelayの違い
  • 複数メソッドの挙動(並列 vs. 順番待ち)
  • スケジューラの並列化設定方法
    など、現場で使える知識を丁寧に解説します!

✅ 事前準備:スケジューリングを有効にする

まず、@Scheduled を使うには明示的にスケジューラ機能を有効化する必要があります。

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // ← これを忘れると定期処理が動かない!
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

✅ 定期実行処理の作成方法(@Scheduledの基本)

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class SampleScheduler {

    @Scheduled(fixedRate = 5000) // 5秒ごとに実行
    public void runTask() {
        System.out.println("実行中: " + java.time.LocalTime.now());
    }
}

✅ Bean登録されているクラスである必要がある

@Scheduled を付けたメソッドは、Spring管理下の「Bean」でなければ実行されません。

🔍 Beanって何?

Beanとは、SpringがDI(依存性注入)の対象として管理するインスタンスのこと。アプリの起動時に自動的に生成・登録されます。

🔧 Beanとして認識されるには?

  • @Component
  • @Service
  • @Repository
  • @Configuration

などのアノテーションを付けることで、自動でBean登録されます。

@Component // これがないと動かない!
public class MyScheduler {
    @Scheduled(fixedRate = 10000)
    public void run() {
        System.out.println("10秒ごとに実行!");
    }
}

✅ 各種 @Scheduled のオプションと違い

🔁 fixedRate:開始時間からの間隔で実行(非同期でも可)

@Scheduled(fixedRate = 5000)
public void task1() {}

🔁 fixedDelay:前回処理の「終了から○ms後」に実行

@Scheduled(fixedDelay = 5000)
public void task2() {}

⏱ initialDelay:最初だけ遅らせてから開始

@Scheduled(initialDelay = 3000, fixedRate = 10000)
public void task3() {}

✅ cron式を使った柔軟な定期処理

@Scheduled(cron = "0 0 0 * * *") // 毎日0時に実行
public void runDaily() {}
曜日指定方法
月〜日MON〜SUN
毎分0 * * * * *
毎週月曜9時0 0 9 * * MON

✅ デフォルトは単一スレッド!複数メソッドは順番待ち!

Spring Bootでは、@Scheduled メソッドはすべて単一のスレッドで実行されます。

つまり?

複数の @Scheduled メソッドが同じタイミングで起動しても、一つずつしか実行されません

@Scheduled(fixedRate = 1000)
public void taskA() {
    System.out.println("タスクA開始");
    Thread.sleep(3000); // 時間かかる処理
    System.out.println("タスクA終了");
}

@Scheduled(fixedRate = 1000)
public void taskB() {
    System.out.println("タスクB実行");
}

🧨 上記の例では…

  • taskA が3秒かかると、
  • taskB の実行は そのあと(順番待ち) になる。

❗ これは意図しない「実行遅延」や「詰まり」の原因になります。

✅ 8. スケジューラを並列化(マルチスレッド)する方法【詳細解説】

🔒 デフォルトでは「シングルスレッド」

Spring Bootの @Scheduled は、特に設定しない場合、1本のスレッドしか使われません。
そのため、複数の @Scheduled メソッドが同じタイミングで発火しても、順番に1つずつしか処理されないのです。

🧪 例:2つのタスクが重なると…

@Scheduled(fixedRate = 1000)
public void longTask() throws InterruptedException {
    System.out.println("LongTask START");
    Thread.sleep(5000); // 処理に5秒かかる
    System.out.println("LongTask END");
}

@Scheduled(fixedRate = 1000)
public void quickTask() {
    System.out.println("QuickTask");
}

実行結果(一部)

LongTask START
(1秒後) ← QuickTask は実行されない
(5秒後)LongTask END
QuickTask ← ここでやっと実行!

👉 QuickTask が「後回し」になり、定期実行にならない!

✅ 解決策:ThreadPoolTaskScheduler を使って並列化

SchedulingConfigurer インタフェースを使い、スケジューラのスレッドプールを自分で定義すれば、複数タスクを同時に実行できます。

🔧 実装例:SchedulerConfigクラス

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Configuration
public class SchedulerConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar registrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();

        scheduler.setPoolSize(5); // 並列に最大5スレッドまで処理可能
        scheduler.setThreadNamePrefix("scheduled-task-");
        scheduler.initialize();

        registrar.setTaskScheduler(scheduler);
    }
}

✅ 効果:

  • @Scheduled メソッドが同時に別スレッドで実行される
  • 時間がかかる処理が他の定期タスクをブロックしない

✅ 重要ポイント

項目説明
setPoolSize(int)並列で実行できる最大スレッド数(多すぎ注意)
setThreadNamePrefixログなどでスレッド名が見えるようにすると便利
initialize()スケジューラを起動する前に必ず呼ぶ
registrar.setTaskScheduler()作成したスケジューラをSpringに登録

📈 スケーラブルな設計になる

この方法を使えば:

  • @Scheduled メソッドが独立して並列に実行可能
  • 処理が重いスケジューラが他に影響しない
  • 複数のタスクを同時に定期実行できる

⚠️ スレッドを増やすとサーバーにどれくらい負荷がかかるのか?

ThreadPoolTaskSchedulersetPoolSize() でスレッド数を増やすことで、確かに並列実行が可能になりますが、次のようなリスクや注意点もあります。

🔻 1. メモリ消費が増える

  • スレッド1つあたり、1MB〜数MB程度のスタックメモリを消費します。
  • 10スレッドなら約10MB〜30MB、100スレッドなら数百MB以上に。
  • サーバーのメモリが潤沢でない場合、OutOfMemoryErrorのリスクも。

🔻 2. CPUへの負荷が高くなる

  • スレッド数を増やしても、CPUコア数を超えると実行は待機状態に。
  • 例えば、4コアのCPUに20スレッドを同時起動させても、4つしか実行されず16は待ち状態
  • 無駄にスレッドを増やすとコンテキストスイッチ(切り替え)コストが増えるため、逆に処理が遅くなる可能性あり。

🔻 3. スレッド間の同期・ロックが複雑化する

  • 並列化が進むと、スレッド間で同じリソースにアクセスする場面が出てきます。
  • その結果:
    • データ競合(race condition)
    • ロックによるデッドロック
    • 排他制御によるパフォーマンス低下
  • ➜ マルチスレッドにするほど設計とテストが難しくなる

✅ 適切なスレッド数の目安

サーバー構成おすすめのスレッド数(例)
ローカルPC(4コア/8GB)2〜4程度
小規模サーバー(2vCPU/4GB)2〜3程度
商用サーバー(8vCPU/16GB〜)5〜10程度(要チューニング)

💡 重要:PoolSize = コア数 × 2 程度を上限の目安とするのが一般的です

✅ スレッド数を増やす際のチェックポイント

  • ✅ 並列処理する「タスク」の処理時間と重さを見積もる
  • ✅ アプリケーションが使用する他の非同期処理(例:@Async, ExecutorService)とバランスをとる
  • ✅ JVMの最大メモリやCPUコア数に合わせて上限を設定
  • ✅ 本番環境ではモニタリング(JVM, CPU, Heap, スレッド数)を必ず導入

✅ 9. よくあるトラブルと解決法まとめ

問題原因
@Scheduled が動かない@EnableScheduling を忘れている
メソッドが呼ばれない@Component などが付いておらず Bean登録されていない
処理が遅延する・詰まる単一スレッドで順番待ち → スケジューラの並列化が必要
時間がズレるcron式が UTC基準になっている → タイムゾーン指定が必要

✅ 10. まとめ

項目ポイント
@Scheduled を使うには@EnableScheduling が必要
Bean登録が必須@Component@Service をつけよう
デフォルトは1スレッド複数メソッドで詰まりが発生 → 並列化設定が有効
並列実行が必要ならThreadPoolTaskScheduler をカスタム設定
cron式毎日・毎時など柔軟なスケジュールが可能

コメント

タイトルとURLをコピーしました