ExecutorService を一言でいうと
ExecutorService は、
「スレッドを自分で new せずに、”仕事(タスク)だけ” を投げれば、いい感じに別スレッドで実行してくれる仕組み」
です。
new Thread(...).start() をあちこちで書く代わりに、
- 実行したい処理だけを
RunnableやCallableに書いて - それを
ExecutorServiceに渡す
ことで、スレッドの管理(生成・再利用・終了など)を一括で任せられます。
「スレッドの集合(スレッドプール)」に対して、「タスク」を投げるイメージを持ってください。
なぜ ExecutorService が必要なのか(生 Thread の問題点)
new Thread() を乱発するとどうなるか
初心者のうちは、並行処理を書こうとしてこうしがちです。
new Thread(() -> {
// 重い処理
}).start();
Javaこれ自体は間違いではありませんが、
大量にこういうコードがあると、次の問題が出てきます。
- どのスレッドがいつ終わるのか把握しづらい
- スレッド数が増えすぎて、CPU を無駄に消費したり、コンテキストスイッチだらけになる
- 例外処理や終了処理をバラバラに書くことになり、管理が大変
つまり、「スレッドという資源を雑に使いがち」になります。
ExecutorService の発想:スレッドは“プール”、あなたは“仕事”だけ渡す
ExecutorService を使うと、
スレッドの生成・再利用・終了は「ひとつの場所」で管理できるようになります。
あなたがやるのは、
- 「やってほしい仕事(タスク)」を
Runnable/Callableとして書く - そのタスクを
executor.submit(...)やexecutor.execute(...)で投げる
だけです。
スレッドを何本用意するか、
タスクをどのスレッドに割り当てるか、
終わったスレッドを再利用するか、
といった「インフラ側の話」は、ExecutorService 側が引き受けてくれます。
一番基本:固定スレッドプールを作ってタスクを実行する
Executors.newFixedThreadPool でスレッドプールを作る
まずは、固定数のスレッドを持つ ExecutorService を作る例から。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3); // スレッド3本のプール
for (int i = 1; i <= 10; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("タスク " + taskId + " を実行中: " +
Thread.currentThread().getName());
try {
Thread.sleep(1000); // 1秒かかる仕事だとする
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("タスク " + taskId + " 完了: " +
Thread.currentThread().getName());
});
}
executor.shutdown(); // もう新しいタスクは受け付けない、完了したら終了
}
}
Javaここで起きていることを、言葉で整理します。
newFixedThreadPool(3)で、「3 本だけスレッドを使うプール」を作る- 10 個のタスクを
submitで投げる - 同時に走るのは最大 3 個まで(他はキューで順番待ち)
- タスクが終わったスレッドは、次のタスクに再利用される
- 最後に
shutdown()して、全部終わったらスレッドが終了する
new Thread(...).start() だと 10 本スレッドが立ってしまいますが、
この例では「3 本で回す」ことができます。
CPU コア数や負荷に応じて、
「何本のスレッドでタスクを回すか」を制御できることが、ExecutorService の大きな価値です。
submit と Future:結果を受け取る・例外を知る
Runnable と Callable の違い
executor.submit(...) に渡せるのは Runnable だけではありません。Callable<V> という「戻り値を返せるタスク」も渡せます。
Runnable
→ run() メソッド、戻り値なし、例外はチェック例外を投げられない
Callable<V>
→ V call() throws Exception メソッド、戻り値あり、例外を投げられる
submit はタスクの完了を表す Future<V> を返してくれます。
例:数値計算タスクの結果を Future で受け取る
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class CallableExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
int n = i;
Future<Integer> future = executor.submit(() -> {
System.out.println("計算タスク " + n + " 実行中");
Thread.sleep(1000);
return n * n; // n の2乗を返す
});
futures.add(future);
}
for (Future<Integer> f : futures) {
Integer result = f.get(); // 完了を待って結果を受け取る
System.out.println("結果: " + result);
}
executor.shutdown();
}
}
Javaここで重要なポイントは、
submitはすぐ戻ってくる(非同期にタスクが開始される)Future#get()を呼ぶと、そのタスクが終わるまで待ち、結果を返してくれる- タスク内で例外が起きた場合は、
get()時にExecutionExceptionとして検知できる
ということです。
new Thread(...).start() だと、「そのスレッドでの結果や例外を、呼び出し側から扱いにくい」ですが、
ExecutorService + Future だと、「タスクの完了・結果・失敗」をオブジェクトとして扱えるようになります。
shutdown / shutdownNow とライフサイクル管理
ExecutorService を作ったら、必ず「どこで止めるか」を決める
ExecutorService は内部でスレッドを持っているので、
使い終わったら止めてあげないとスレッドリークにつながります。
基本の流れはこうです。
ExecutorService executor = Executors.newFixedThreadPool(3);
// タスクをいろいろ submit する
executor.shutdown(); // 新規タスク受付停止、実行中・待ち行列分が終わるまで待つ
Javashutdown() は「柔らかい停止」です。
すでにキューに入っているタスクはそのまま最後まで実行されます。
完全に終わったかどうか確認したいときは、awaitTermination を使うこともあります。
executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // まだ終わらないなら、強制停止を試みる
}
JavashutdownNow は「強制寄り」の停止
shutdownNow() は、実行待ちのタスクをキャンセルし、
実行中のスレッドに割り込み(interrupt)をかけます。
List<Runnable> notStartedTasks = executor.shutdownNow();
JavanotStartedTasks には、まだ始まっていなかったタスクが返ってきます。
強制停止なので、
「タスクが割り込みにきちんと反応するように書かれているか」
が重要になります。
初心者のうちは、
- 基本は
shutdown()で穏やかに終了 - 本当に止まらない時だけ
shutdownNow()
くらいの意識で十分です。
ExecutorService のよくある種類(ざっくりイメージ)
newFixedThreadPool:固定本数で回す
すでに出てきた newFixedThreadPool(n) は、
「常に最大 n 本のスレッドでタスクを処理する」プールです。
CPU コア数や負荷を見て、「このくらいで回したい」という時に使います。
newCachedThreadPool:必要に応じてスレッド増やして、アイドルは破棄
newCachedThreadPool() は、
「必要に応じてスレッドを増やし、使われないスレッドは破棄する」という、伸び縮みするプールです。
短命なタスクを大量にさばきたいが、数は一定ではない、
といったケースに向きます。
ただし、「思った以上にスレッドが増えすぎる」危険もあるので、
初心者のうちは固定プールの方が挙動を理解しやすいです。
newSingleThreadExecutor:1 本だけの順次実行
newSingleThreadExecutor() は、
1 本だけスレッドを持ち、タスクを一つずつ順番に処理する ExecutorService です。
シングルスレッドなのに Executor を使う意味は、
- タスクのキューイングや終了処理を Executor に任せられる
ExecutorServiceとしての共通 API(submit, shutdown)が使える
といったところにあります。
GUI アプリの「イベントディスパッチスレッド」のようなイメージで、
「常に一つずつ順番に処理したい」場面で使えます。
ExecutorService を使うときに意識したいこと
「スレッドを意識するコード」と「仕事の中身」を分離できる
ExecutorService を使うと、
スレッド数を何本にするか
タスクをどのスレッドで実行するか
といった「並行実行の戦略」は Executor に任せ、
「タスクの中で何をするか」は Runnable / Callable に閉じ込める
という分離ができます。
これにより、
- 並行処理の設計と、ビジネスロジックの設計を混ぜずに済む
- テスト時には単一スレッドの Executor に差し替える、といったこともやりやすくなる
といったメリットが生まれます。
例外処理・キャンセル・タイムアウトまで含めて「タスク」の単位で考える
ExecutorService を使うと、「タスク」が一つの単位になります。
- そのタスクが失敗したとき、どこでログを取るか
- どこまで結果を待って、どこからタイムアウト扱いにするか
- 途中でキャンセルできるようにするか(
Future#cancel)
などを、「タスクごと」に設計できます。
例えば、ある計算は 3 秒まで待つが、それ以上かかったら諦めたい、といったとき、
Future<Result> f = executor.submit(callableTask);
try {
Result r = f.get(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
f.cancel(true); // 割り込みをかけてキャンセル試行
}
Javaのように、「待つ側の都合」に合わせた書き方ができます。
まとめ:ExecutorService 概要を自分の中でこう位置づける
ExecutorService を初心者向けにまとめると、
「スレッドを自前で管理せず、“タスク” という単位で並行処理を投げられる仕組み(スレッドプールの顔をした実行サービス)」
です。
押さえておきたいのは、
Executors.newFixedThreadPoolなどでプールを作る- 実行したい処理は
Runnable/Callableに書き、submitで投げる - 戻り値や例外は
Future経由で扱える - 使い終わったら
shutdown()(必要ならshutdownNow())で止める - 「スレッドをどう使うか」と「何をするか」をきれいに分離できる
