概要
Thread を直接 new してタスクごとに生成・破棄すると、スレッド生成のオーバーヘッドが無視できなくなり、同時実行数の制御も困難です。ExecutorService はスレッドプールを管理し、タスクの投入と結果の取得を分離する仕組みを提供します。固定サイズのプール、キャッシュプール、シングルスレッドプールなど用途別のファクトリが用意されており、タスクの結果は Future を通じてタイムアウト付きで取得できます。この記事では、各プールの特性と使い分け、shutdown の正しいタイミング、Future.get のタイムアウト制御までを、業務で頻出するパターンに絞って整理します。
使いどころ
帳票生成バッチでスレッドプールサイズを固定し、DB コネクション数の上限を超えないよう並列度を制御する
外部 API への並列呼び出しを ExecutorService に投入し、Future.get でタイムアウト付きで結果を集約する
定期実行バッチを ScheduledExecutorService で管理し、cron の代わりにアプリ内でスケジュールを制御する
コード例
import java.util.ArrayList;
import java.util.concurrent.*;
public class ExecutorServiceDemo {
public static void main(String[] args) throws Exception {
System.out.println("=== 固定スレッドプール ===");
var executor = Executors.newFixedThreadPool(3);
try {
var futures = new ArrayList<Future<String>>();
for (var i = 1; i <= 5; i++) {
var taskName = "タスク-" + i;
var future = executor.submit(() -> {
Thread.sleep(100);
return "完了: " + taskName + " by "
+ Thread.currentThread().getName();
});
futures.add(future);
}
for (var future : futures) {
System.out.println(future.get());
}
} finally {
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
}
System.out.println("\n=== タイムアウト付き Future.get() ===");
var timeoutExecutor = Executors.newSingleThreadExecutor();
try {
var future = timeoutExecutor.submit(() -> {
Thread.sleep(500);
return "重い処理の結果";
});
try {
var result = future.get(200, TimeUnit.MILLISECONDS);
System.out.println(result);
} catch (TimeoutException e) {
System.out.println("タイムアウト → キャンセル");
future.cancel(true);
}
} finally {
timeoutExecutor.shutdown();
}
}
}Version Coverage
ラムダ式 + var + record で簡潔に記述可能。record でタスク情報を保持する設計パターンが使える。
// Java 17: ラムダ式 + record で簡潔に
record Task(String name, long sleepMs) {}
var task = new Task("タスク-1", 100);
var future = executor.submit(() -> {
Thread.sleep(task.sleepMs());
return "完了: " + task.name();
});Library Comparison
注意点
shutdown() を呼ばないとスレッドプールが生き残り、アプリケーションが終了しない。try-finally で必ず shutdown する
newCachedThreadPool はタスク数に応じてスレッドを無制限に作成するため、大量タスクを投入すると OutOfMemoryError の原因になる
Future.get() にタイムアウトを設定しないと、タスクが完了しない限りメインスレッドが永久にブロックされる
ExecutorService を static フィールドに持つ場合、アプリ終了時の shutdown 呼び出しを忘れやすい。ShutdownHook の登録を検討する
submit() の戻り値(Future)を握りつぶすと、タスク内の例外が検知されない。少なくとも Future.get() で例外の有無を確認する
FAQ
並列度の上限を制御したいときは FixedThreadPool、短時間のタスクが散発的に発生する場面では CachedThreadPool が向いています。CachedThreadPool は上限なしにスレッドを作るため、タスク量が読めない場合は避けてください。
shutdown() は新規タスクの受付を停止し、投入済みタスクの完了を待ちます。shutdownNow() は実行中タスクへの interrupt を試み、未実行タスクのリストを返します。通常は shutdown + awaitTermination を使います。
API は互換性がありますが、synchronized ブロック内でのブロッキングが仮想スレッドのキャリアスレッドをピン留めする問題があります。性能テストで確認してから移行してください。