java-recipes

ホーム マルチスレッド › Th-05

Th-05: ExecutorService / スレッドプール管理

new Thread() を毎回呼ぶとスレッドの生成・破棄コストがかかります。ExecutorService はスレッドプールを管理し、タスクをキューに積んで効率よく処理します。Future を使うと非同期タスクの結果をあとから取得でき、タイムアウトの設定も可能です。

なぜスレッドプールが必要か

スレッドを毎回 new Thread() で作ると、OS レベルのスレッド生成コストが発生します。 スレッドプールはあらかじめスレッドを一定数用意しておき、タスクが来るたびに既存のスレッドを再利用します。 これにより生成コストを抑え、スレッド数を制御してシステムリソースを保護できます。

メソッド特徴向いている場面
newFixedThreadPool(n)スレッド数を n で固定。キューにタスクを積むCPU 負荷を制御したい場合
newCachedThreadPool()必要に応じてスレッドを生成・再利用。上限なし短い処理が大量に来る場合
newSingleThreadExecutor()スレッド1本でタスクを順番に処理順番を保証したい場合
newVirtualThreadPerTaskExecutor() (Java 21+)タスクごとに仮想スレッドを割り当てブロッキング I/O を多用する大量タスク

Future による非同期結果取得

executor.submit(callable)Future<T> を返します。future.get() を呼ぶとタスクの完了を待機して結果を取得できます。 タイムアウトを指定すれば、処理が長引いた場合に TimeoutException を投げさせることもできます。

Future<String> future = executor.submit(() -> "結果");

// タイムアウトなし(完了まで待機)
String result = future.get();

// タイムアウトあり(200ms 以内に完了しなければ TimeoutException)
String result = future.get(200, TimeUnit.MILLISECONDS);

サンプルコード

Sample.java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class ExecutorServiceSample {

    // タスク: 処理時間のシミュレーション
    static class SlowTask implements Callable<String> {
        private final String name;
        private final long sleepMs;

        public SlowTask(String name, long sleepMs) {
            this.name = name;
            this.sleepMs = sleepMs;
        }

        @Override
        public String call() throws Exception {
            Thread.sleep(sleepMs);
            return "完了: " + name + " by " + Thread.currentThread().getName();
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("=== 固定スレッドプール ===");
        ExecutorService executor = Executors.newFixedThreadPool(3);

        try {
            List<Future<String>> futures = new ArrayList<Future<String>>();
            for (int i = 1; i <= 5; i++) {
                Future<String> future = executor.submit(new SlowTask("タスク-" + i, 100));
                futures.add(future);
            }

            for (Future<String> future : futures) {
                System.out.println(future.get()); // 結果を待機して取得
            }
        } finally {
            executor.shutdown(); // ✅ 必ず shutdown() する
            executor.awaitTermination(5, TimeUnit.SECONDS);
        }

        System.out.println("\n=== キャッシュスレッドプール ===");
        ExecutorService cached = Executors.newCachedThreadPool();
        try {
            Future<String> f1 = cached.submit(new SlowTask("キャッシュA", 50));
            Future<String> f2 = cached.submit(new SlowTask("キャッシュB", 50));
            System.out.println(f1.get());
            System.out.println(f2.get());
        } finally {
            cached.shutdown();
        }

        System.out.println("\n=== タイムアウト付き Future.get() ===");
        ExecutorService timeoutExecutor = Executors.newSingleThreadExecutor();
        try {
            Future<String> future = timeoutExecutor.submit(new SlowTask("重いタスク", 500));
            try {
                String result = future.get(200, TimeUnit.MILLISECONDS); // 200ms でタイムアウト
                System.out.println(result);
            } catch (TimeoutException e) {
                System.out.println("タイムアウト発生 → future.cancel()");
                future.cancel(true);
            }
        } finally {
            timeoutExecutor.shutdown();
        }
    }
}

よくあるミス・注意点

⚠️ shutdown() を忘れてプロセスが終了しない

ExecutorService のスレッドはデーモンスレッドではないため、shutdown()を呼ばないとメインメソッドが終わってもプロセスが生き続けます。 必ず finally ブロックで shutdown() を呼んでください。

ExecutorService executor = Executors.newFixedThreadPool(3);
try {
    // タスクを submit ...
} finally {
    executor.shutdown(); // ✅ 新規タスクの受け付けを停止
    executor.awaitTermination(5, TimeUnit.SECONDS); // 最大5秒待機
}

⚠️ Future.get() のループ内でデッドロックが発生するパターン

固定スレッドプールでタスクを実行中に、そのタスクが新たなタスクを submit してget() で待機しようとすると、すべてのスレッドが互いに待ち合うデッドロックになります。 タスクを入れ子にする場合は、別の ExecutorService を使うか、 タスクの設計を見直してください。

テストする観点

  • Future.get() でタスクの戻り値が正しく取得できること
  • ✅ タイムアウト値を超えた場合に TimeoutException が発生すること
  • future.cancel(true) を呼んだ後、future.isCancelled()true を返すこと
  • shutdown() を呼んだ後に submit() すると RejectedExecutionException が発生すること
  • newFixedThreadPool(n) では同時実行スレッド数が n を超えないこと

GitHub でソースコードを見る →