導入
Javaでの並行処理において、手動で「new Thread()」を繰り返すのは、リソース管理の観点から推奨されません。スレッドの生成・破棄はコストが高く、管理を怠るとシステム全体のパフォーマンス低下やメモリ不足(OutOfMemoryError)を招きます。本記事では、Javaの並行処理の基盤である「ExecutorService」を活用し、いかに効率的かつ安全に非同期タスクを管理するかを解説します。
基礎知識
ExecutorServiceとは、スレッドのライフサイクル(作成、実行、終了)を開発者に代わって管理するフレームワークです。
スレッドプールという概念が重要で、あらかじめ用意されたスレッドの集まりを使い回すことで、スレッド生成コストを削減します。
また、Java 21以降では「Virtual Threads(仮想スレッド)」の登場により、従来の重いプラットフォームスレッド(OSスレッドと1対1)から、極めて軽量なスレッドへとパラダイムがシフトしています。
実装/解決策
ExecutorServiceを利用するには、Executorsファクトリクラスを使用してスレッドプールを作成します。主なパターンは以下の通りです。
1. FixedThreadPool: 指定した数のスレッドでタスクを処理。
2. CachedThreadPool: 必要に応じてスレッドを生成し、不要になったら破棄。
3. VirtualThreadPerTaskExecutor: Java 21以降で使用可能。タスクごとに仮想スレッドを割り当てる(スレッド作成コストがほぼゼロ)。
サンプルプログラム
以下は、ExecutorServiceを使用して複数のタスクを並行処理する実用的な例です。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorSample {
public static void main(String[] args) {
// Java 21以降であれば Executors.newVirtualThreadPerTaskExecutor() を推奨します
// 今回は汎用的な固定スレッドプールを使用します
try (ExecutorService executor = Executors.newFixedThreadPool(3)) {
for (int i = 1; i <= 5; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("タスク " + taskId + " を実行中: " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 擬似的な重い処理
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 新しいタスクの受付を停止
executor.shutdown();
}
// try-with-resourcesにより、自動的にexecutor.close()が呼ばれ安全に終了します
System.out.println("すべてのタスクが終了しました。");
}
}
応用・注意点
現場で活用する際の重要な注意点を3つ挙げます。
1. 終了処理(Shutdown)の重要性: ExecutorServiceは明示的に終了させないと、JVMが停止しません。必ず「shutdown()」を呼び出すか、Java 19以降のAutoCloseableインターフェースを活用してください。
2. 例外ハンドリング: submit()で投げられたタスク内の例外は、デフォルトではコンソールに出力されず握りつぶされることがあります。Future.get()で結果を確認するか、タスク内でtry-catchを行うのが安全です。
3. 構造化並行処理(Structured Concurrency): Java 21でプレビュー機能として導入された「Structured Concurrency」は、複数のサブタスクを一つのユニットとして管理し、エラー時のキャンセルの伝播を容易にします。最新のJava環境であれば、従来のExecutorServiceと併せて学習することをお勧めします。
適切なスレッド管理は、堅牢なシステム構築の第一歩です。まずは小さなタスクからExecutorServiceへの移行を始めてみてください。

コメント