皆さん、こんにちは!Javaで並行処理や非同期処理を実装する際に、`ExecutorService`は欠かせない存在です。でも、「`submit()`と`execute()`って何が違うの?」「`invokeAll()`や`invokeAny()`はどんな時に使うの?」といった疑問を持っていませんか?
この記事では、Javaのシニアエンジニアが、皆さんの疑問を解決し、`ExecutorService`を効果的に使いこなせるようになるためのTipsをお届けします。
なぜExecutorServiceが重要なのか?
現代のアプリケーションでは、複数の処理を同時に実行することで、ユーザー体験の向上やシステム全体のパフォーマンス改善が期待できます。例えば、Webアプリケーションで複数のユーザーからのリクエストを同時に処理したり、重い計算処理をバックグラウンドで実行したりする場合などが挙げられます。
`ExecutorService`は、これらの並行処理を効率的かつ安全に管理するためのフレームワークです。スレッドの生成・管理・再利用を抽象化してくれるため、開発者はスレッドの詳細に煩わされることなく、ビジネスロジックに集中できます。
ExecutorServiceの基礎知識
`ExecutorService`は、タスク(RunnableまたはCallable)を実行するためのインターフェースです。タスクを実行するスレッドプールを管理し、タスクの実行を非同期で行います。
- Runnable: `run()`メソッドを持ち、実行される処理を定義しますが、戻り値はありません。
- Callable: `call()`メソッドを持ち、実行される処理を定義し、実行結果を戻り値として返します。例外もスローできます。
- Future: 非同期処理の結果を表すオブジェクトです。タスクの実行が完了したかどうかを確認したり、結果を取得したりできます。
ExecutorServiceの主要メソッド:execute() vs submit()
`ExecutorService`には、タスクを実行するための主に2つのメソッドがあります。
1. `execute(Runnable command)`
このメソッドは、`Runnable`タスクを実行します。戻り値はなく、タスクの完了を待つこともできません。タスクが正常に完了したか、例外が発生したかを知る手段も提供されません。単純にタスクをスレッドプールに渡して実行させる場合に利用します。
2. `submit(Runnable task)` / `submit(Callable task)` / `submit(Runnable task, T result)`
`submit()`メソッドは、`execute()`よりも高機能です。
- `submit(Runnable task)`: `Runnable`タスクを実行し、`Future>`を返します。この`Future`オブジェクトを通じて、タスクの実行状態を確認したり、キャンセルしたりできます。ただし、`Runnable`なので結果は取得できません。
- `submit(Callable
task)`: `Callable`タスクを実行し、`Future `を返します。この`Future`オブジェクトを使って、タスクの実行結果(`T`型)を取得したり、例外を捕捉したりできます。 - `submit(Runnable task, T result)`: `Runnable`タスクを実行し、タスクが完了した際に指定した`result`を返します。`Runnable`なので、タスク自体の戻り値はありません。
使い分けのポイント:
- タスクの実行結果や完了状態を知る必要がある場合は `submit()` を使用します。
- 単純にタスクを実行したいだけで、結果や完了を気にする必要がない場合は `execute()` を使用します。
複数のタスクをまとめて実行:invokeAll() と invokeAny()
`ExecutorService`は、複数のタスクをまとめて実行するための便利なメソッドも提供しています。
1. `invokeAll(Collection
このメソッドは、指定された`Callable`タスクのコレクションをすべて実行し、すべてのタスクが完了するまで待機します。各タスクに対応する`Future`オブジェクトのリストを返します。この`Future`リストを通じて、個々のタスクの結果や例外を取得できます。
2. `invokeAny(Collection
このメソッドは、指定された`Callable`タスクのコレクションを実行し、いずれか1つのタスクが正常に完了した時点で、そのタスクの実行結果を返します。他のタスクは実行を継続するかもしれませんが、結果としては最初に完了したタスクのものだけが返されます。いずれのタスクも例外をスローした場合、その例外がスローされます。
使い分けのポイント:
- すべてのタスクの実行結果を確認したい場合は `invokeAll()` を使用します。
- いずれか1つのタスクが完了すれば良い場合や、最も早く完了したタスクの結果を利用したい場合は `invokeAny()` を使用します。
サンプルプログラム
ここでは、`submit()`と`execute()`、そして`invokeAll()`を使った簡単な例を示します。
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.;
public class ExecutorServiceExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 固定スレッドプールを作成(コア数3、最大数3のスレッドを持つ)
ExecutorService executor = Executors.newFixedThreadPool(3);
// — submit() の例 (Callable) —
System.out.println(“— submit() with Callable —“);
Callable
System.out.println(“Callableタスク実行中…”);
Thread.sleep(1000); // 1秒待機
return “Callableタスク完了!”;
};
// Callableタスクをsubmitし、Futureを取得
Future
// タスクの完了を待ち、結果を取得
String result = futureResult.get(); // get()はタスク完了までブロックする
System.out.println(“取得した結果: ” + result);
// — submit() の例 (Runnable) —
System.out.println(“\n— submit() with Runnable —“);
Runnable runnableTask = () -> {
System.out.println(“Runnableタスク実行中…”);
try {
Thread.sleep(500); // 0.5秒待機
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(“Runnableタスク完了。”);
};
// Runnableタスクをsubmitし、Futureを取得(結果は取得できない)
Future> runnableFuture = executor.submit(runnableTask);
// 必要であれば、runnableFuture.get() でタスク完了を待つことも可能
// runnableFuture.get(); // 結果はnullになる
// — execute() の例 —
System.out.println(“\n— execute() —“);
Runnable simpleRunnableTask = () -> {
System.out.println(“execute()で実行されるタスク開始…”);
try {
Thread.sleep(700); // 0.7秒待機
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(“execute()で実行されるタスク終了。”);
};
// execute()でRunnableタスクを実行。戻り値なし。
executor.execute(simpleRunnableTask);
// — invokeAll() の例 —
System.out.println(“\n— invokeAll() —“);
Callable
System.out.println(“invokeAllタスク1開始…”);
Thread.sleep(1500);
System.out.println(“invokeAllタスク1完了。”);
return “結果1”;
};
Callable
System.out.println(“invokeAllタスク2開始…”);
Thread.sleep(800);
System.out.println(“invokeAllタスク2完了。”);
return “結果2”;
};
Collection
// invokeAllは、すべてのタスクが完了するまでブロックする
List
System.out.println(“invokeAll完了。各タスクの結果を取得します:”);
for (Future
// get()は既に完了しているので即座に返る
System.out.println(” – ” + future.get());
}
// ExecutorServiceをシャットダウンする(新しいタスクは受け付けない)
executor.shutdown();
// シャットダウン後、すべてのタスクが完了するのを待つ(タイムアウト付き)
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println(“タスクがタイムアウトしました。強制終了します。”);
executor.shutdownNow(); // 実行中のタスクを強制終了
}
System.out.println(“\nExecutorServiceの処理がすべて完了しました。”);
}
}
このサンプルコードでは、`Executors.newFixedThreadPool(3)`で3つのスレッドを持つスレッドプールを作成しています。`submit()`でCallableとRunnableタスクを実行し、`execute()`でRunnableタスクを実行しています。また、`invokeAll()`で複数のCallableタスクをまとめて実行し、その結果を取得しています。
応用・注意点
- `shutdown()`と`shutdownNow()`: `ExecutorService`の利用が終わったら、必ず`shutdown()`を呼び出して、新しいタスクの受け入れを停止し、実行中のタスクの完了を待つようにしましょう。もし、タスクが完了しない場合に強制的に終了させたい場合は`shutdownNow()`を使用しますが、これは実行中のタスクに`interrupt()`を送信するため、注意が必要です。
- 例外処理: `Callable`タスクで発生した例外は、`Future.get()`を呼び出した際に`ExecutionException`としてラップされてスローされます。`Runnable`タスクで発生した例外は、`execute()`の場合はデフォルトではコンソールに出力されますが、`submit()`の場合は`Future.get()`で取得しようとした際に`ExecutionException`としてスローされます。
- `invokeAny()`の注意点: `invokeAny()`は、いずれかのタスクが例外をスローした場合、他のタスクの実行が完了していても、すぐに例外をスローして終了します。そのため、すべてのタスクの実行結果を確実に得たい場合には適していません。
- スレッドプールの選択: `Executors`クラスには、`newFixedThreadPool()`(固定サイズ)、`newCachedThreadPool()`(必要に応じてスレッドを増減)、`newSingleThreadExecutor()`(単一スレッド)など、様々なスレッドプールを生成するファクトリメソッドがあります。タスクの性質やシステムのリソースに合わせて適切なプールを選択することが重要です。
`ExecutorService`を理解し、これらのメソッドを適切に使い分けることで、Javaでの並行処理・非同期処理をより効率的かつ堅牢に実装できるようになります。ぜひ、皆さんの開発に取り入れてみてください!

コメント