ThreadPoolExecutor
追加のスレッドが開始される前にキューが制限されていっぱいである必要がある場合に、この制限を回避するにはどうすればよいですか。
私は最終的に、によるこの制限に対するややエレガントな(多分少しハックな)解決策を見つけたと思いますThreadPoolExecutor
。これは、拡張含まれLinkedBlockingQueue
、それが返すようにfalse
するためにqueue.offer(...)
、すでにいくつかのタスクがキューイングがあるとき。現在のスレッドがキューに入れられたタスクに追いついていない場合、TPEは追加のスレッドを追加します。プールがすでに最大スレッドにある場合、RejectedExecutionHandler
が呼び出されます。その後put(...)
、キューに入れるハンドラです。
offer(...)
戻ることができfalse
、put()
ブロックしないキューを作成するのは確かに奇妙なので、これはハック部分です。しかし、これはキューのTPEの使用法でうまく機能するため、これを実行しても問題はありません。
これがコードです:
// extend LinkedBlockingQueue to force offer() to return false conditionally
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
private static final long serialVersionUID = -6903933921423432194L;
@Override
public boolean offer(Runnable e) {
// Offer it to the queue if there is 0 items already queued, else
// return false so the TPE will add another thread. If we return false
// and max threads have been reached then the RejectedExecutionHandler
// will be called which will do the put into the queue.
if (size() == 0) {
return super.offer(e);
} else {
return false;
}
}
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1 /*core*/, 50 /*max*/,
60 /*secs*/, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// This does the actual put into the queue. Once the max threads
// have been reached, the tasks will then queue up.
executor.getQueue().put(r);
// we do this after the put() to stop race conditions
if (executor.isShutdown()) {
throw new RejectedExecutionException(
"Task " + r + " rejected from " + e);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
});
このメカニズムにより、タスクをキューに送信すると、次のThreadPoolExecutor
ことが行われます。
- スレッド数を最初にコアサイズ(ここでは1)まで拡大します。
- キューにそれを提供します。キューが空の場合、既存のスレッドによって処理されるようにキューに入れられます。
- キューにすでに1つ以上の要素がある場合、
offer(...)
はfalseを返します。
- falseが返された場合、最大数(ここでは50)に達するまで、プール内のスレッド数を増やします。
- 最大の場合、それは
RejectedExecutionHandler
RejectedExecutionHandler
その後、FIFO順で最初に使用可能なスレッドによって処理されるキューにタスクを置きます。
上記のコード例では、キューは制限なしですが、制限付きキューとして定義することもできます。たとえば、1000の容量を追加すると、LinkedBlockingQueue
次のようになります。
- スレッドを最大に拡大する
- その後、1000タスクでいっぱいになるまで待ちます
- 次に、キューでスペースが利用可能になるまで、呼び出し元をブロックします。
また、で使用する必要がある場合offer(...)
は
、タイムアウトとしての代わりRejectedExecutionHandler
にoffer(E, long, TimeUnit)
メソッドを使用できますLong.MAX_VALUE
。
警告:
シャットダウン後にexecutor にタスクが追加されることが予想される場合は、executor-serviceがシャットダウンされたときにRejectedExecutionException
カスタムからスローする方が賢明なRejectedExecutionHandler
場合があります。これを指摘してくれた@RaduToaderに感謝します。
編集:
この回答に対する別の微調整は、アイドルスレッドがあるかどうかをTPEに尋ね、存在する場合にのみ項目をエンキューすることです。このための真のクラスを作成し、それにourQueue.setThreadPoolExecutor(tpe);
メソッドを追加する必要があります。
その場合、offer(...)
メソッドは次のようになります。
tpe.getPoolSize() == tpe.getMaximumPoolSize()
その場合、単にを呼び出すかどうかを確認してくださいsuper.offer(...)
。
- そうでない場合は
tpe.getPoolSize() > tpe.getActiveCount()
、super.offer(...)
アイドルスレッドのように見えるので呼び出します。
- それ以外の場合は、
false
別のスレッドのforkに戻ります。
多分これ:
int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
return super.offer(e);
} else {
return false;
}
TPEのgetメソッドは、volatile
フィールドにアクセスするか、(の場合getActiveCount()
)TPEをロックしてスレッドリストをウォークするため、負荷が高いことに注意してください。また、タスクが不適切にキューに入れられたり、アイドルスレッドがあったときに別のスレッドが分岐したりする可能性がある競合状態もあります。