別のタイプのスレッド プールは「キャッシュ」です。このようなスレッド プールは、固定スレッド プールと同様に一般的に使用されます。
名前が示すように、この種類のスレッド プールはスレッドをキャッシュします。新しいタスクを実行するためにそれらのスレッドを再利用するために、未使用のスレッドを一定期間存続させます。このようなスレッド プールは、適度な量の軽い作業がある場合に最適です。
「ある程度の妥当な量」の意味はかなり広いですが、そのようなプールはあらゆる数のタスクに適しているわけではないことを知っておく必要があります。たとえば、100 万個のタスクを作成するとします。それぞれにかかる時間が非常に短い場合でも、不当な量のリソースが使用され、パフォーマンスが低下します。また、I/O タスクなど、実行時間が予測できない場合にも、このようなプールを避ける必要があります。
内部では、ThreadPoolExecutorコンストラクターが次の引数を使用して呼び出されます。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
次の値が引数としてコンストラクターに渡されます。
パラメータ | 価値 |
---|---|
corePoolSize ( executorサービスの開始時に準備完了 (開始) されるスレッドの数) | 0 |
MaximumPoolSize (実行サービスが作成できるスレッドの最大数) | 整数.MAX_VALUE |
keepAliveTime (スレッド数がcorePoolSizeより大きい場合に、解放されたスレッドが破棄されるまで存続する時間) | 60L |
単位(時間の単位) | 時間単位.SECONDS |
workQueue (キューの実装) | new SynchronousQueue<Runnable>() |
また、まったく同じ方法で独自のThreadFactory実装を渡すことができます。
SynchronousQueue について話しましょう
同期転送の基本的な考え方は非常に単純ですが、直感に反しています (つまり、直感や常識からそれは間違っていると判断されます)。別のスレッドがキューで要素を受信した場合にのみ、要素をキューに追加できます。同時。つまり、新しいタスクが到着するとすぐに、実行中のスレッドがすでにそのタスクを選択しているため、同期キューにタスクを含めることはできません。
新しいタスクがキューに入ると、プール内に空いているアクティブなスレッドがあれば、そのタスクが選択されます。すべてのスレッドがビジー状態の場合は、新しいスレッドが作成されます。
キャッシュされたプールはゼロスレッドから始まり、Integer.MAX_VALUEスレッドまで増加する可能性があります。基本的に、キャッシュされたスレッド プールのサイズはシステム リソースによってのみ制限されます。
システム リソースを節約するために、キャッシュされたスレッド プールは 1 分間アイドル状態のスレッドを削除します。
実際にどのように機能するかを見てみましょう。ユーザーリクエストをモデル化するタスククラスを作成します。
public class Task implements Runnable {
int taskNumber;
public Task(int taskNumber) {
this.taskNumber = taskNumber;
}
@Override
public void run() {
System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
}
}
mainメソッドでは、 newCachedThreadPoolを作成し、実行する 3 つのタスクを追加します。ここではサービスのステータスを出力します(1)。
次に、30 秒間一時停止し、別のタスクを開始して、ステータスを表示します(2)。
その後、メインスレッドを 70 秒間一時停止し、ステータスを出力します(3)。その後、再び 3 つのタスクを追加し、ステータスを再度出力します(4)。
タスクを追加した直後にステータスを表示する場所では、最新の出力を得るために最初に 1 秒間のスリープを追加します。
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
executorService.submit(new Task(i));
}
TimeUnit.SECONDS.sleep(1);
System.out.println(executorService); //(1)
TimeUnit.SECONDS.sleep(30);
executorService.submit(new Task(3));
TimeUnit.SECONDS.sleep(1);
System.out.println(executorService); //(2)
TimeUnit.SECONDS.sleep(70);
System.out.println(executorService); //(3)
for (int i = 4; i < 7; i++) {
executorService.submit(new Task(i));
}
TimeUnit.SECONDS.sleep(1);
System.out.println(executorService); //(4)
executorService.shutdown();
そして結果は次のとおりです。
プール 1 スレッド 2 スレッドで
ユーザー リクエスト #1 を処理しました プール 1 スレッド 3 スレッドでユーザー リクエスト #2 を処理しました
(1) java.util.concurrent .ThreadPoolExecutor@f6f4d33[実行中、プール サイズ = 3、アクティブなスレッド = 0、キューに入れられたタスク = 0、完了したタスク = 3] プール-1-
スレッド-2 スレッド
(2) java.util.concurrent でユーザー要求 #3 を処理しました。 ThreadPoolExecutor@f6f4d33[実行中、プール サイズ = 3、アクティブなスレッド = 0、キューに入れられたタスク = 0、完了したタスク = 4] (3)
java.util.concurrent.ThreadPoolExecutor@f6f4d33[実行中、プール サイズ = 0、アクティブなスレッド = 0 、キューに入れられたタスク = 0、完了したタスク = 4]
プール 1 スレッド 4 のスレッドでユーザー リクエスト #4 を処理しました
プール 1 スレッド 5 のスレッドでユーザー リクエスト #5 を処理しました
プール-1-スレッド-4 スレッドでユーザー要求 #6 を処理しました
(4) java.util.concurrent.ThreadPoolExecutor@f6f4d33[実行中、プール サイズ = 2、アクティブなスレッド = 0、キューに入れられたタスク = 0、完了したタスク = 7]
それぞれの手順を見てみましょう。
ステップ | 説明 |
---|---|
1 (3 つのタスクが完了した後) | 3 つのスレッドを作成し、これら 3 つのスレッド上で 3 つのタスクが実行されました。 ステータスが表示されると、3 つのタスクがすべて完了し、スレッドは他のタスクを実行できる状態になります。 |
2 (30 秒間の一時停止と別のタスクの実行後) | 非アクティブな状態が 30 秒続いた後も、スレッドはまだ生きていてタスクを待機しています。 別のタスクが追加され、残りのライブ スレッドのプールから取得されたスレッド上で実行されます。 新しいスレッドはプールに追加されませんでした。 |
3 (70 秒の一時停止後) | スレッドはプールから削除されました。 タスクを受け入れる準備ができているスレッドがありません。 |
4 (さらに 3 つのタスクを実行した後) | さらに多くのタスクを受信した後、新しいスレッドが作成されました。今回は 2 つのスレッドだけで 3 つのタスクを処理できました。 |
さて、これで、別の種類のエグゼキューター サービスのロジックについて理解できました。
Executorsユーティリティ クラスの他のメソッドと同様に、newCachedThreadPoolメソッドにも、 ThreadFactoryオブジェクトを引数として受け取るオーバーロードされたバージョンがあります。
GO TO FULL VERSION