1. 導入:なぜScheduledExecutorServiceが必要なのか
Javaで「一定時間後に処理を実行したい」「決まった間隔で定期的にタスクを回したい」という要件は、バックグラウンド処理や監視タスクで頻繁に発生します。かつてはjava.util.Timerを使っていましたが、これは例外発生時にスレッドが終了してしまう等の課題がありました。ScheduledExecutorServiceは、より堅牢で柔軟な並行処理を可能にし、現代のJava開発における非同期処理の標準的な解決策となっています。
2. 基礎知識:ScheduledExecutorServiceの仕組み
ScheduledExecutorServiceは、ExecutorServiceを拡張したインターフェースです。主な特徴は以下の通りです。
・スレッドプール管理: 複数のスレッドを再利用できるため、大量のタスクが発生してもリソース消費を抑えられます。
・例外耐性: タスク内で例外が発生しても、タイマー自体が停止することなく、次の実行へ安全に引き継がれます。
・柔軟なスケジュール: 指定時間後の単発実行(schedule)や、固定間隔での繰り返し実行(scheduleAtFixedRate / scheduleWithFixedDelay)を選択できます。
3. 実装と解決策
主にExecutorsクラスのファクトリメソッドを使用してインスタンス化します。定期実行には以下の2つのメソッドを使い分けます。
・scheduleAtFixedRate: 前回のタスク開始時刻を基準に、一定間隔で実行します。タスクが長引くと実行間隔が詰まる可能性があります。
・scheduleWithFixedDelay: 前回のタスク終了時刻を基準に、一定の待機時間を挟んで実行します。タスクの処理時間に左右されず、安定した間隔を保ちたい場合に適しています。
4. サンプルプログラム
以下のコードは、ScheduledExecutorServiceを用いて1秒の遅延後に3秒間隔で処理を実行する例です。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledTaskExample {
public static void main(String[] args) {
// スレッドプールを1つ作成(定期実行には単一スレッドで十分なことが多い)
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
System.out.println("タスクのスケジュールを開始します...");
// 1秒後に開始し、その後3秒間隔で実行する
scheduler.scheduleAtFixedRate(() -> {
try {
System.out.println("定期処理を実行中: " + System.currentTimeMillis());
// ここに実際のロジックを記述
} catch (Exception e) {
// 例外をキャッチすることで、スケジューラが停止するのを防ぐ
e.printStackTrace();
}
}, 1, 3, TimeUnit.SECONDS);
// 15秒後に終了するシャットダウン処理
scheduler.schedule(() -> {
System.out.println("スケジューラを終了します。");
scheduler.shutdown();
}, 15, TimeUnit.SECONDS);
}
}
5. 応用・注意点:現場で陥りやすい罠
・シャットダウンの忘れ: スケジューラを適切に停止(shutdown)しないと、JVMが終了できずメモリリークの原因になります。try-finallyブロックで確実にシャットダウンを呼び出しましょう。
・スレッドのブロック: タスク内で重い同期処理やブロッキングI/Oを行うと、スケジューラ全体の実行が遅延します。もしタスクが非常に重い場合は、CompletableFutureと組み合わせて非同期に結果を待つか、別のExecutorServiceへタスクを委譲することを検討してください。
・Virtual Threadsとの関係: Java 21以降、Virtual Threadsが利用可能ですが、ScheduledExecutorServiceのタスクとしてVirtual Threadsを利用する場合は、Executors.newVirtualThreadPerTaskExecutor()と組み合わせてタスクを投げる設計が、高並行環境ではより効率的です。

コメント