【Java学習|実務向け】Java並行処理の「終わらせ方」をマスターする:ExecutorServiceの適切な終了処理

1. 導入:なぜExecutorServiceの終了処理が重要なのか

Javaの並行処理において、スレッドプール(ExecutorService)を正しく終了させることは非常に重要です。開発中、アプリケーションを停止してもプロセスが終了しなかったり、逆にタスクが中途半端に終了してデータ整合性が損なわれたりした経験はありませんか?これらは多くの場合、終了処理の不備が原因です。本記事では、安全かつ確実にスレッドプールを停止するための作法を解説します。

2. 基礎知識:スレッドプールのライフサイクル

ExecutorServiceは、一度作成すると自身では停止しません。アプリケーションのライフサイクルに合わせて明示的に停止させる必要があります。
shutdown(): 新しいタスクの受付を停止しますが、既に投入済みのタスクは最後まで実行します。
shutdownNow(): 実行中のタスクに対して割り込み(interrupt)を試み、キュー内のタスクを未実行のまま返却します。
awaitTermination(): 指定した時間まで、終了処理が完了するのをブロックして待ちます。

3. 実装/解決策:推奨される終了処理のパターン

現場でのベストプラクティスは、まずshutdown()を呼び出してタスクの完了を待ち、一定時間経っても終わらない場合にshutdownNow()で強制終了させる「2段階の終了処理」です。

4. サンプルプログラム:安全な終了処理のテンプレート

このコードは、実務でそのまま利用できる定型的な終了処理の実装です。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecutorShutdownExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // タスク投入(省略)
        executor.submit(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("タスク完了");
            } catch (InterruptedException e) {
                System.out.println("タスクが中断されました");
            }
        });

        // 終了処理を開始
        shutdownAndAwaitTermination(executor);
    }

    public static void shutdownAndAwaitTermination(ExecutorService pool) {
        // 新規タスクの受付を無効化
        pool.shutdown(); 
        try {
            // 既存タスクの完了を一定時間待機
            if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
                // タイムアウトした場合は強制終了
                pool.shutdownNow();
                // 強制終了の完了を待機
                if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
                    System.err.println("プールが正常に終了しませんでした");
                }
            }
        } catch (InterruptedException ie) {
            // 現在のスレッドも割り込まれた場合、強制終了
            pool.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

5. 応用・注意点:現場で陥りやすい罠

注意点1:タスク側の割り込み対応
shutdownNow()はThread.interrupt()を呼び出しますが、タスク側で割り込みを無視する実装(無限ループや重いI/O処理)があると終了しません。タスク内では必ずInterruptedExceptionをキャッチするか、定期的にThread.interrupted()を確認してください。

注意点2:Virtual Threadsとの関連
Java 21以降のVirtual Threads(仮想スレッド)を利用する場合も、ExecutorServiceのインターフェースは変わりません。ただし、Virtual Threadsは非常に軽量であるため、大量のExecutorServiceを生成しては破棄するような設計ではなく、アプリケーション全体で共有するスコープを意識することが大切です。また、今後はStructured Concurrencyを活用することで、こうした複雑な終了処理をより直感的に制御できるようになります。

まずは、既存コードの終了処理が上記のテンプレートに従っているか、一度確認してみてください。これだけで、システム終了時の不安定な挙動の多くは解消されるはずです。

コメント

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