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);
サンプルコード
よくあるミス・注意点
⚠️ 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 を超えないこと