Java – ExecutorServiceでマルチスレッド処理を簡潔に記述する
ExecutorServiceは、Javaでマルチスレッド処理を簡潔かつ効率的に管理するためのインターフェースです。
スレッドプールを利用してスレッドの作成や終了を自動的に管理し、リソースの無駄を削減します。
Executorsクラスを使ってインスタンスを生成し、submitやexecuteメソッドでタスクを実行します。
タスク完了後はshutdownでサービスを停止します。
ExecutorServiceとは
ExecutorServiceは、Javaの並行処理を簡素化するためのインターフェースです。
スレッドの管理やタスクの実行を効率的に行うことができ、特にマルチスレッド処理を行う際に非常に便利です。
ExecutorServiceを使用することで、スレッドの生成や管理に関する煩雑なコードを書く必要がなくなり、よりシンプルで可読性の高いプログラムを作成できます。
主な特徴
- スレッドプールの管理: スレッドの生成や終了を自動で行い、リソースの無駄遣いを防ぎます。
- タスクの非同期実行: タスクを非同期で実行し、結果を後で取得することができます。
- 柔軟なタスク管理: タスクのキャンセルや完了の確認が容易です。
以下は、ExecutorServiceを使用して簡単なタスクを実行するサンプルコードです。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App {
public static void main(String[] args) {
// スレッドプールを作成
ExecutorService executorService = Executors.newFixedThreadPool(2);
// タスクを実行
executorService.submit(() -> {
System.out.println("タスク1を実行中");
});
executorService.submit(() -> {
System.out.println("タスク2を実行中");
});
// スレッドプールをシャットダウン
executorService.shutdown();
}
}タスク1を実行中
タスク2を実行中このコードでは、2つのタスクをスレッドプールで同時に実行しています。
ExecutorServiceを使用することで、スレッドの管理が簡単になり、タスクの実行が効率的に行われます。
ExecutorServiceの基本的な使い方
ExecutorServiceを使用することで、マルチスレッド処理を簡潔に記述できます。
以下に、基本的な使い方を説明します。
スレッドプールの作成
ExecutorServiceを使用するには、まずスレッドプールを作成します。
スレッドプールは、同時に実行できるスレッドの数を制限し、リソースの効率的な使用を可能にします。
以下のように、さまざまな種類のスレッドプールを作成できます。
| プールの種類 | 説明 |
|---|---|
newFixedThreadPool(int nThreads) | 固定数のスレッドを持つプールを作成します。 |
newCachedThreadPool() | 必要に応じてスレッドを生成し、再利用します。 |
newSingleThreadExecutor() | 単一のスレッドでタスクを順次実行します。 |
タスクの実行
スレッドプールを作成したら、タスクを実行することができます。
タスクはRunnableまたはCallableインターフェースを実装したオブジェクトとして定義します。
以下は、Runnableを使用したタスクの実行例です。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App {
public static void main(String[] args) {
// 固定数のスレッドプールを作成
ExecutorService executorService = Executors.newFixedThreadPool(2);
// タスクを実行
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("タスク1を実行中");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("タスク2を実行中");
}
});
// スレッドプールをシャットダウン
executorService.shutdown();
}
}タスク1を実行中
タスク2を実行中タスクの結果の取得
Callableインターフェースを使用すると、タスクの実行結果を取得することができます。
submitメソッドはFutureオブジェクトを返し、これを使用して結果を取得します。
以下はその例です。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class App {
public static void main(String[] args) throws Exception {
// 固定数のスレッドプールを作成
ExecutorService executorService = Executors.newFixedThreadPool(2);
// Callableタスクを実行
Future<String> futureResult = executorService.submit(new Callable<String>() {
@Override
public String call() {
return "タスクの結果";
}
});
// 結果を取得
String result = futureResult.get(); // 結果を取得
System.out.println(result);
// スレッドプールをシャットダウン
executorService.shutdown();
}
}タスクの結果このように、ExecutorServiceを使用することで、タスクの実行や結果の取得が簡単に行えます。
スレッドの管理が自動化されるため、開発者はビジネスロジックに集中できます。
タスクの種類と実行方法
ExecutorServiceを使用する際には、主に2つのタスクの種類があります。
それはRunnableとCallableです。
これらのタスクは、異なる目的や結果を持っており、実行方法も若干異なります。
以下にそれぞれの特徴と実行方法を説明します。
Runnableタスク
Runnableは、戻り値を持たないタスクを定義するためのインターフェースです。
runメソッドをオーバーライドして、実行したい処理を記述します。
Runnableタスクは、主に副作用を持つ処理や、結果を必要としない場合に使用されます。
以下は、Runnableを使用したタスクの実行例です。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App {
public static void main(String[] args) {
// スレッドプールを作成
ExecutorService executorService = Executors.newFixedThreadPool(2);
// Runnableタスクを実行
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("Runnableタスクを実行中");
}
});
// スレッドプールをシャットダウン
executorService.shutdown();
}
}Runnableタスクを実行中Callableタスク
Callableは、戻り値を持つタスクを定義するためのインターフェースです。
callメソッドをオーバーライドして、実行したい処理を記述します。
Callableタスクは、計算結果や処理の結果を返す必要がある場合に使用されます。
また、Callableは例外をスローすることができるため、エラーハンドリングが容易です。
以下は、Callableを使用したタスクの実行例です。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class App {
public static void main(String[] args) throws Exception {
// スレッドプールを作成
ExecutorService executorService = Executors.newFixedThreadPool(2);
// Callableタスクを実行
Future<String> futureResult = executorService.submit(new Callable<String>() {
@Override
public String call() {
return "Callableタスクの結果";
}
});
// 結果を取得
String result = futureResult.get(); // 結果を取得
System.out.println(result);
// スレッドプールをシャットダウン
executorService.shutdown();
}
}Callableタスクの結果タスクの実行方法のまとめ
| タスクの種類 | 特徴 | 使用例 |
|---|---|---|
Runnable | 戻り値を持たない、例外をスローしない | 副作用のある処理 |
Callable | 戻り値を持つ、例外をスロー可能 | 計算結果や処理の結果を返す |
このように、RunnableとCallableの2つのタスクを使い分けることで、さまざまな処理を効率的に実行することができます。
ExecutorServiceを利用することで、これらのタスクを簡単に管理し、実行することが可能です。
スレッドプールの種類
ExecutorServiceを使用する際には、さまざまな種類のスレッドプールを選択することができます。
これにより、アプリケーションの要件に応じた最適なスレッド管理が可能になります。
以下に、主要なスレッドプールの種類とその特徴を説明します。
1. 固定スレッドプール (newFixedThreadPool)
固定スレッドプールは、指定した数のスレッドを持ち、タスクがキューに追加されると、空いているスレッドがタスクを実行します。
スレッドの数は固定されているため、リソースの使用が安定します。
特徴
- スレッド数が固定
- リソースの使用が予測可能
- 同時に実行できるタスク数が制限される
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App {
public static void main(String[] args) {
// 固定スレッドプールを作成
ExecutorService executorService = Executors.newFixedThreadPool(3);
// タスクを実行
for (int i = 0; i < 5; i++) {
final int taskId = i;
executorService.submit(() -> {
System.out.println("タスク " + taskId + " を実行中");
});
}
// スレッドプールをシャットダウン
executorService.shutdown();
}
}タスク 0 を実行中
タスク 1 を実行中
タスク 2 を実行中
タスク 3 を実行中
タスク 4 を実行中2. キャッシュスレッドプール (newCachedThreadPool)
キャッシュスレッドプールは、必要に応じてスレッドを生成し、使用後は再利用します。
タスクが少ない場合はスレッドを生成せず、タスクが増えると新しいスレッドを作成します。
特徴
- スレッド数が動的に変化
- 短時間で多くのタスクを処理するのに適している
- 使用されていないスレッドは自動的に終了
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App {
public static void main(String[] args) {
// キャッシュスレッドプールを作成
ExecutorService executorService = Executors.newCachedThreadPool();
// タスクを実行
for (int i = 0; i < 5; i++) {
final int taskId = i;
executorService.submit(() -> {
System.out.println("キャッシュタスク " + taskId + " を実行中");
});
}
// スレッドプールをシャットダウン
executorService.shutdown();
}
}キャッシュタスク 0 を実行中
キャッシュタスク 1 を実行中
キャッシュタスク 2 を実行中
キャッシュタスク 3 を実行中
キャッシュタスク 4 を実行中3. シングルスレッドプール (newSingleThreadExecutor)
シングルスレッドプールは、単一のスレッドでタスクを順次実行します。
タスクがキューに追加されると、1つずつ実行されるため、タスクの実行順序が保証されます。
特徴
- 単一のスレッドで実行
- タスクの実行順序が保証される
- 同時実行は行われない
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App {
public static void main(String[] args) {
// シングルスレッドプールを作成
ExecutorService executorService = Executors.newSingleThreadExecutor();
// タスクを実行
for (int i = 0; i < 5; i++) {
final int taskId = i;
executorService.submit(() -> {
System.out.println("シングルタスク " + taskId + " を実行中");
});
}
// スレッドプールをシャットダウン
executorService.shutdown();
}
}シングルタスク 0 を実行中
シングルタスク 1 を実行中
シングルタスク 2 を実行中
シングルタスク 3 を実行中
シングルタスク 4 を実行中スレッドプールの種類のまとめ
| スレッドプールの種類 | 特徴 | 使用例 |
|---|---|---|
| 固定スレッドプール | スレッド数が固定、リソースの使用が安定 | 同時に実行するタスク数が限られる場合 |
| キャッシュスレッドプール | スレッド数が動的に変化、短時間で多くのタスクを処理 | 短時間で多くのタスクを処理する場合 |
| シングルスレッドプール | 単一のスレッドで順次実行、実行順序が保証 | タスクの実行順序が重要な場合 |
これらのスレッドプールを適切に選択することで、アプリケーションのパフォーマンスを最適化し、リソースの効率的な使用が可能になります。
実践的な使用例
ExecutorServiceを使用した実践的な例として、複数のタスクを並行して実行し、結果を集約するシナリオを考えてみましょう。
この例では、複数の数値の平方を計算し、その結果をリストにまとめて表示します。
例: 数値の平方を計算するタスク
以下のコードでは、Callableを使用して数値の平方を計算するタスクを作成し、ExecutorServiceを使って並行処理を行います。
計算結果はFutureオブジェクトを通じて取得し、最終的に結果を表示します。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class App {
public static void main(String[] args) throws Exception {
// スレッドプールを作成
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 計算する数値のリスト
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Future<Integer>> futures = new ArrayList<>();
// 各数値の平方を計算するタスクを作成
for (Integer number : numbers) {
Callable<Integer> task = () -> {
return number * number; // 数値の平方を計算
};
futures.add(executorService.submit(task)); // タスクを実行
}
// 結果を集約
for (Future<Integer> future : futures) {
Integer result = future.get(); // 結果を取得
System.out.println("計算結果: " + result);
}
// スレッドプールをシャットダウン
executorService.shutdown();
}
}計算結果: 1
計算結果: 4
計算結果: 9
計算結果: 16
計算結果: 25このコードでは、以下の手順で処理を行っています。
- 固定スレッドプールを作成し、同時に3つのタスクを実行できるようにします。
- 計算する数値のリストを用意し、各数値の平方を計算する
Callableタスクを作成します。 - 各タスクを
ExecutorServiceに提出し、Futureオブジェクトをリストに保存します。 - 最後に、各
Futureから計算結果を取得し、表示します。
このように、ExecutorServiceを使用することで、複数のタスクを効率的に並行処理し、結果を集約することができます。
特に、計算やI/O処理などの時間がかかるタスクを扱う際に、パフォーマンスの向上が期待できます。
まとめ
この記事では、JavaのExecutorServiceを利用したマルチスレッド処理の基本から、タスクの種類やスレッドプールの種類、実践的な使用例までを紹介しました。
これにより、スレッド管理の効率化やタスクの並行処理がどのように実現できるかを具体的に理解できたことでしょう。
今後は、実際のプロジェクトにおいてExecutorServiceを活用し、より効率的なプログラムを作成してみてください。