エグゼキュータnewCachedThreadPool()とnewFixedThreadPool()
1. 概要
スレッドプールの実装に関しては、Java標準ライブラリには豊富なオプションが用意されています。 固定およびキャッシュされたスレッドプールは、これらの実装の中でかなり遍在しています。
このチュートリアルでは、スレッドプールが内部でどのように機能しているかを確認し、これらの実装とそのユースケースを比較します。
2. キャッシュされたスレッドプール
Executors.newCachedThreadPool()を呼び出すと、Javaがキャッシュされたスレッドプールを作成する方法を見てみましょう。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
キャッシュされたスレッドプールは、「同期ハンドオフ」を使用して新しいタスクをキューに入れています。 同期ハンドオフの基本的な考え方は単純ですが、直感に反します。別のスレッドが同時にアイテムを取得する場合にのみ、アイテムをキューに入れることができます。 つまり、SynchronousQueueはタスクを一切保持できません。
新しいタスクが入ってくるとしましょう。
キャッシュされたプールはゼロスレッドで始まり、Integer.MAX_VALUEスレッドを持つようになる可能性があります。 実際には、キャッシュされたスレッドプールの唯一の制限は、使用可能なシステムリソースです。
システムリソースをより適切に管理するために、キャッシュされたスレッドプールは、1分間アイドル状態のままになっているスレッドを削除します。
2.1. ユースケース
キャッシュされたスレッドプール構成は、スレッド(名前の由来)を短時間キャッシュして、他のタスクに再利用します。
ここで重要なのは「合理的」で「短命」です。 この点を明確にするために、キャッシュされたプールが適切でないシナリオを評価してみましょう。 ここでは、100万のタスクを送信します。各タスクの完了には100マイクロ秒かかります。
Callable<String> task = () -> {
long oneHundredMicroSeconds = 100_000;
long startedAt = System.nanoTime();
while (System.nanoTime() - startedAt <= oneHundredMicroSeconds);
return "Done";
};
var cachedPool = Executors.newCachedThreadPool();
var tasks = IntStream.rangeClosed(1, 1_000_000).mapToObj(i -> task).collect(toList());
var result = cachedPool.invokeAll(tasks);
これにより、不当なメモリ使用量につながる多くのスレッドが作成され、さらに悪いことに、多くのCPUコンテキストスイッチが作成されます。 これらの異常は両方とも、全体的なパフォーマンスを大幅に低下させます。
したがって、IOバウンドタスクのように実行時間が予測できない場合は、このスレッドプールを回避する必要があります。
3. 固定スレッドプール
固定スレッドプールが内部でどのように機能するかを見てみましょう。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
キャッシュされたスレッドプールとは対照的に、これは、期限が切れないスレッドの数が固定された無制限のキューを使用しています
その結果、固定スレッドプールは、実行時間が予測できないタスクに適しています。
4. 不幸な類似点
これまでのところ、キャッシュされたスレッドプールと固定スレッドプールの違いのみを列挙してきました。
これらすべての違いはさておき、両方とも使用されています AbortPolicy 彼らのように飽和ポリシー
現実の世界で何が起こるか見てみましょう。
キャッシュされたスレッドプールは、極端な状況でもますます多くのスレッドを作成し続けるため、実際には、飽和点に達することはありません。 同様に、固定スレッドプールは、キューにますます多くのタスクを追加し続けます。 したがって、固定プールも飽和点に達することはありません。
両方のプールが飽和状態にならないため、負荷が非常に高い場合、スレッドの作成やタスクのキューイングのために大量のメモリを消費します。 負傷に侮辱を加えると、キャッシュされたスレッドプールにも多くのプロセッサコンテキストスイッチが発生します。
とにかく、でリソース消費をより細かく制御するには、カスタム ThreadPoolExecutorを作成することを強くお勧めします。
var boundedQueue = new ArrayBlockingQueue<Runnable>(1000);
new ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, new AbortPolicy());
ここで、スレッドプールは最大20のスレッドを持つことができ、最大1000のタスクのみをキューに入れることができます。 また、それ以上の負荷を受け入れることができない場合は、単に例外をスローします。
5. 結論
このチュートリアルでは、JDKソースコードを覗いて、さまざまなExecutorsが内部でどのように機能するかを確認しました。 次に、固定スレッドプールとキャッシュスレッドプール、およびそれらのユースケースを比較しました。
最後に、カスタムスレッドプールを使用して、これらのプールの制御不能なリソース消費に対処しようとしました。