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
を活用し、より効率的なプログラムを作成してみてください。