【Java学習|実務向け】Java並行処理の現場解釈:ExecutorServiceの使い分けと最新トレンド

導入

Javaでのマルチスレッド処理において、スレッド管理を自前で行うのは非常にリスクが高い作業です。そこで活用されるのがExecutorServiceですが、特にExecutorsクラスが提供するファクトリメソッドは便利である反面、安易な選択はシステムトラブルの元になります。本稿では、レガシーなExecutorServiceの適切な使い分けと、Java 21以降の現代的な並行処理への移行戦略について解説します。

基礎知識

JavaにおけるスレッドはOSのネイティブスレッドと1対1で対応しており、生成コストが非常に高いリソースです。
Executors.newFixedThreadPool(n)は、固定数のスレッドでタスクを処理します。タスクが溢れると内部のキュー(LinkedBlockingQueue)に溜まるため、メモリ枯渇に注意が必要です。
Executors.newCachedThreadPool()は、必要に応じてスレッドを生成します。スレッド数は無制限に増える可能性があるため、高負荷時にOutOfMemoryErrorを引き起こすリスクがあります。
Executors.newSingleThreadExecutor()は、単一スレッドでタスクを逐次実行します。タスクの実行順序を保証したい場合に適しています。

実装/解決策

実務では、単にファクトリメソッドを使うのではなく、ThreadPoolExecutorを直接インスタンス化し、適切なキューサイズと拒否ポリシー(RejectedExecutionHandler)を設定することを強く推奨します。これにより、システムが許容できる負荷を明示的に制御できます。

サンプルプログラム

以下は、FixedThreadPoolをより安全に運用するための構成例です。

import java.util.concurrent.;

public class ThreadPoolExample {
public static void main(String[] args) {
// 固定スレッドプールにキュー上限を設ける設定
// キャパシティを制限することで、メモリ枯渇を防ぐ
int corePoolSize = 4;
int maxPoolSize = 4;
long keepAliveTime = 0L;
int queueCapacity = 100;

ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new ThreadPoolExecutor.CallerRunsPolicy() // キューが溢れたら呼び出し元スレッドで実行させる
);

executor.submit(() -> {
System.out.println(“タスク実行中: ” + Thread.currentThread().getName());
});

executor.shutdown();
}
}

応用・注意点

現代のJava開発において、これらのスレッドプールをそのまま使い続けることには限界があります。特に、I/O待ちが多いタスクにはVirtual Threads(仮想スレッド)の導入を検討してください。

Java 21から導入された仮想スレッドは、従来のプラットフォームスレッドとは異なり、非常に軽量です。これまでは「スレッドを使い回す」ためにプールが必要でしたが、仮想スレッドであれば「タスクごとにスレッドを生成する」のが基本戦略となります。

注意点:
1. CompletableFutureとの併用: 既存のExecutorServiceを使いつつ、非同期処理を繋げる場合はCompletableFutureが便利ですが、ブロッキング操作を混ぜるとスレッドプールが枯渇する「スレッド飢餓」が起きやすいです。
2. Structured Concurrency: Java 21以降のStructuredTaskScopeを利用することで、複数のタスクを親子関係として管理し、例外発生時のキャンセル処理を簡潔に書くことが可能です。

結論として、レガシーなExecutorServiceは「タスクの種類」と「リソースの限界値」を厳密に定義して使用し、新規開発では可能な限りVirtual Threadsへの移行を計画することをお勧めします。

コメント

タイトルとURLをコピーしました