【Java学習|豆知識】Java並行処理の要!ExecutorServiceでスレッド管理をスマートに

導入

Javaでの並行処理において、手動で「new Thread()」を繰り返すのは、リソース管理の観点から推奨されません。スレッドの生成・破棄はコストが高く、管理を怠るとシステム全体のパフォーマンス低下やメモリ不足(OutOfMemoryError)を招きます。本記事では、Javaの並行処理の基盤である「ExecutorService」を活用し、いかに効率的かつ安全に非同期タスクを管理するかを解説します。

基礎知識

ExecutorServiceとは、スレッドのライフサイクル(作成、実行、終了)を開発者に代わって管理するフレームワークです。
スレッドプールという概念が重要で、あらかじめ用意されたスレッドの集まりを使い回すことで、スレッド生成コストを削減します。
また、Java 21以降では「Virtual Threads(仮想スレッド)」の登場により、従来の重いプラットフォームスレッド(OSスレッドと1対1)から、極めて軽量なスレッドへとパラダイムがシフトしています。

実装/解決策

ExecutorServiceを利用するには、Executorsファクトリクラスを使用してスレッドプールを作成します。主なパターンは以下の通りです。

1. FixedThreadPool: 指定した数のスレッドでタスクを処理。
2. CachedThreadPool: 必要に応じてスレッドを生成し、不要になったら破棄。
3. VirtualThreadPerTaskExecutor: Java 21以降で使用可能。タスクごとに仮想スレッドを割り当てる(スレッド作成コストがほぼゼロ)。

サンプルプログラム

以下は、ExecutorServiceを使用して複数のタスクを並行処理する実用的な例です。

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

public class ExecutorSample {
    public static void main(String[] args) {
        // Java 21以降であれば Executors.newVirtualThreadPerTaskExecutor() を推奨します
        // 今回は汎用的な固定スレッドプールを使用します
        try (ExecutorService executor = Executors.newFixedThreadPool(3)) {
            for (int i = 1; i <= 5; i++) {
                int taskId = i;
                executor.submit(() -> {
                    System.out.println("タスク " + taskId + " を実行中: " + Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000); // 擬似的な重い処理
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
            // 新しいタスクの受付を停止
            executor.shutdown();
        } 
        // try-with-resourcesにより、自動的にexecutor.close()が呼ばれ安全に終了します
        System.out.println("すべてのタスクが終了しました。");
    }
}

応用・注意点

現場で活用する際の重要な注意点を3つ挙げます。

1. 終了処理(Shutdown)の重要性: ExecutorServiceは明示的に終了させないと、JVMが停止しません。必ず「shutdown()」を呼び出すか、Java 19以降のAutoCloseableインターフェースを活用してください。
2. 例外ハンドリング: submit()で投げられたタスク内の例外は、デフォルトではコンソールに出力されず握りつぶされることがあります。Future.get()で結果を確認するか、タスク内でtry-catchを行うのが安全です。
3. 構造化並行処理(Structured Concurrency): Java 21でプレビュー機能として導入された「Structured Concurrency」は、複数のサブタスクを一つのユニットとして管理し、エラー時のキャンセルの伝播を容易にします。最新のJava環境であれば、従来のExecutorServiceと併せて学習することをお勧めします。

適切なスレッド管理は、堅牢なシステム構築の第一歩です。まずは小さなタスクからExecutorServiceへの移行を始めてみてください。

コメント

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