多くの人が使用するThreadPoolExecutor
スレッドプールをサポートするExecutorService
のデフォルトの動作にしばらくイライラしています。 Javadocsから引用するには:
CorePoolSizeを超えて実行されているmaximumPoolSizeスレッドよりも少ない場合、新しいスレッドが作成されますキューがいっぱいの場合のみ。
つまり、次のコードでスレッドプールを定義すると、LinkedBlockingQueue
が無制限であるため、2番目のスレッドが never 開始されます。
ExecutorService threadPool =
new ThreadPoolExecutor(1 /*core*/, 50 /*max*/, 60 /*timeout*/,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(/* unlimited queue */));
bounded キューがあり、キューが full である場合のみ、コア番号を超えるスレッドが開始されます。多数のジュニアJavaマルチスレッドプログラマはThreadPoolExecutor
のこの動作を認識していないと思われます。
今、私はこれが最適でない特定のユースケースを持っています。独自のTPEクラスを作成せずに、それを回避する方法を探しています。
私の要件は、おそらく信頼性の低いサードパーティにコールバックするWebサービスです。
newFixedThreadPool(...)
を使用したくありません。newCachedThreadPool()
。ThreadPoolExecutor
でこの制限を回避するにはどうすればよいでしょうか。キューをバインドし、] /より多くのスレッドを開始する必要があります。 キューイングタスクの前にさらにスレッドを開始するにはどうすればよいですか?
編集:
@Flavioは、ThreadPoolExecutor.allowCoreThreadTimeOut(true)
を使用してコアスレッドのタイムアウトと終了を行うことについて良い点を示しています。私はそれを考えましたが、それでもコアスレッド機能が必要でした。可能であれば、プール内のスレッドの数がコアサイズを下回らないようにしました。
スレッドを開始する前にキューを制限していっぱいにする必要がある
ThreadPoolExecutor
でこの制限を回避するにはどうすればよいですか。
ThreadPoolExecutor
を使用して、この制限に対するややエレガントな(おそらく少しハッキングな)ソリューションをようやく見つけたと思います。 LinkedBlockingQueue
を拡張して、いくつかのタスクが既にキューに入れられているときにqueue.offer(...)
に対してfalse
を返すようにします。現在のスレッドがキューに入れられたタスクに追いついていない場合、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);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
});
_
このメカニズムを使用すると、タスクをキューに送信すると、ThreadPoolExecutor
は次のことを行います。
offer(...)
はfalseを返します。RejectedExecutionHandler
を呼び出しますRejectedExecutionHandler
はタスクをキューに入れ、最初に利用可能なスレッドによってFIFOの順序で処理されるようにします。上記の例のコードでは、キューは無制限ですが、制限付きキューとして定義することもできます。たとえば、LinkedBlockingQueue
に1000の容量を追加すると、次のようになります。
さらに、RejectedExecutionHandler
でoffer(...)
を本当に使用する必要がある場合は、_Long.MAX_VALUE
_をタイムアウトとして代わりにoffer(E, long, TimeUnit)
メソッドを使用できます。
編集:
@Ralfのフィードバックごとにoffer(...)
メソッドのオーバーライドを調整しました。これは、負荷に追いついていない場合にのみ、プール内のスレッド数をスケールアップします。
編集:
この答えのもう1つの微調整は、アイドルスレッドがあるかどうかを実際にTPEに尋ね、存在する場合にのみアイテムをキューに入れることです。このための真のクラスを作成し、ourQueue.setThreadPoolExecutor(tpe);
メソッドを追加する必要があります。
offer(...)
メソッドは次のようになります。
tpe.getPoolSize() == tpe.getMaximumPoolSize()
がsuper.offer(...)
を呼び出すだけかどうかを確認してください。tpe.getPoolSize() > tpe.getActiveCount()
は、アイドルスレッドがあるように見えるため、super.offer(...)
を呼び出します。false
を返して別のスレッドをフォークします。たぶんこれ:
_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をロックしてスレッドリストを調べるため、高価です。また、ここには競合状態があり、タスクが不適切にキューに入れられたり、アイドルスレッドがあったときに別のスレッドが分岐したりする可能性があります。
この質問には他にも2つの答えがありますが、これが一番いいと思います。
現在受け入れられている答え のテクニックに基づいています。つまり:
offer()
メソッドをオーバーライドして、(時々)falseを返します。ThreadPoolExecutor
が新しいスレッドを生成するか、タスクを拒否します。RejectedExecutionHandler
を実際にに設定し、拒否時にタスクをキューに入れます。問題は、offer()
がfalseを返す必要がある場合です。現在受け入れられている答えは、キューにタスクがいくつかある場合にfalseを返しますが、コメントで指摘したように、これは望ましくない結果を引き起こします。あるいは、常にfalseを返す場合、キューで待機しているスレッドがある場合でも、新しいスレッドを生成し続けます。
解決策は、Java 7 LinkedTransferQueue
を使用し、offer()
call tryTransfer()
を使用することです。それ以外の場合、offer()
はfalseを返し、ThreadPoolExecutor
は新しいスレッドを生成します。
BlockingQueue<Runnable> queue = new LinkedTransferQueue<Runnable>() {
@Override
public boolean offer(Runnable e) {
return tryTransfer(e);
}
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
コアサイズと最大サイズを同じ値に設定し、allowCoreThreadTimeOut(true)
を使用してコアスレッドをプールから削除できるようにします。
注:現在、私は my other answer を好み、推奨しています。
これは私にもっと簡単に感じられるバージョンです:新しいタスクが実行されるたびにcorePoolSizeを(maximumPoolSizeの制限まで)増やし、その後、タスクが完了します。
別の言い方をすれば、実行中またはキューに入れられたタスクの数を追跡し、ユーザーが指定した「コアプールサイズ」とmaximumPoolSizeの間である限り、corePoolSizeがタスクの数と等しくなるようにします。
_public class GrowBeforeQueueThreadPoolExecutor extends ThreadPoolExecutor {
private int userSpecifiedCorePoolSize;
private int taskCount;
public GrowBeforeQueueThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
userSpecifiedCorePoolSize = corePoolSize;
}
@Override
public void execute(Runnable runnable) {
synchronized (this) {
taskCount++;
setCorePoolSizeToTaskCountWithinBounds();
}
super.execute(runnable);
}
@Override
protected void afterExecute(Runnable runnable, Throwable throwable) {
super.afterExecute(runnable, throwable);
synchronized (this) {
taskCount--;
setCorePoolSizeToTaskCountWithinBounds();
}
}
private void setCorePoolSizeToTaskCountWithinBounds() {
int threads = taskCount;
if (threads < userSpecifiedCorePoolSize) threads = userSpecifiedCorePoolSize;
if (threads > getMaximumPoolSize()) threads = getMaximumPoolSize();
setCorePoolSize(threads);
}
}
_
書かれているように、クラスは、構築後にユーザーが指定したcorePoolSizeまたはmaximumPoolSizeの変更をサポートせず、直接またはremove()
またはpurge()
を介した作業キューの操作をサポートしません。
追加のThreadPoolExecutor
を取り、creationThreshold
をオーバーライドするexecute
のサブクラスがあります。
public void execute(Runnable command) {
super.execute(command);
final int poolSize = getPoolSize();
if (poolSize < getMaximumPoolSize()) {
if (getQueue().size() > creationThreshold) {
synchronized (this) {
setCorePoolSize(poolSize + 1);
setCorePoolSize(poolSize);
}
}
}
}
多分それも助けになるかもしれませんが、もちろんあなたのものはもっと芸術的です...
推奨される回答は、JDKスレッドプールの問題のうち1つだけを解決します。
JDKスレッドプールはキューイングに偏っています。したがって、新しいスレッドを作成する代わりに、タスクをキューに入れます。キューが制限に達した場合のみ、スレッドプールは新しいスレッドを生成します。
負荷が軽くなると、スレッドの廃止は行われません。たとえば、プールに到達するジョブのバーストが原因でプールが最大になり、その後に一度に最大2タスクの軽負荷が発生した場合、プールはすべてのスレッドを使用して軽負荷を処理し、スレッドのリタイアを防ぎます。 (必要なスレッドは2つだけです...)
上記の動作に不満があるため、先に進み、上記の欠陥を克服するためにプールを実装しました。
解決するには2)Lifoスケジューリングを使用すると、問題が解決します。このアイデアは、ACM applicative 2015カンファレンスでBen Maurerによって提示されました: Systems @ Facebook scale
そこで、新しい実装が生まれました。
これまでのところ、この実装は [〜#〜] zel [〜#〜] の非同期実行パフォーマンスを改善します。
実装はスピン可能で、コンテキストスイッチのオーバーヘッドを削減し、特定のユースケースで優れたパフォーマンスを実現します。
それが役に立てば幸い...
PS:JDKフォーク結合プールはExecutorServiceを実装し、「通常の」スレッドプールとして機能します。実装はパフォーマンスが高く、LIFOスレッドスケジューリングを使用しますが、内部キューサイズ、リタイアタイムアウトを制御できません。 ..、そして最も重要なことには、タスクをキャンセルするときにタスクを中断できない
注:現在、私は my other answer を好み、推奨しています。
キューを変更してfalseを返すという元のアイデアに従って、別の提案があります。この1つでは、すべてのタスクがキューに入ることができますが、タスクがexecute()
の後にキューに入れられるたびに、キューが拒否するセンチネルno-opタスクが続き、新しいスレッドが生成され、実行されますno-opの直後にキューから何かが続きます。
ワーカースレッドはLinkedBlockingQueue
を新しいタスクのためにポーリングしている可能性があるため、利用可能なスレッドがある場合でもタスクがキューに登録される可能性があります。使用可能なスレッドがある場合でも新しいスレッドを生成しないようにするには、キューで新しいタスクを待機しているスレッドの数を追跡し、キューに待機中のスレッドよりも多くのタスクがある場合にのみ新しいスレッドを生成する必要があります。
final Runnable SENTINEL_NO_OP = new Runnable() { public void run() { } };
final AtomicInteger waitingThreads = new AtomicInteger(0);
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
@Override
public boolean offer(Runnable e) {
// offer returning false will cause the executor to spawn a new thread
if (e == SENTINEL_NO_OP) return size() <= waitingThreads.get();
else return super.offer(e);
}
@Override
public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
try {
waitingThreads.incrementAndGet();
return super.poll(timeout, unit);
} finally {
waitingThreads.decrementAndGet();
}
}
@Override
public Runnable take() throws InterruptedException {
try {
waitingThreads.incrementAndGet();
return super.take();
} finally {
waitingThreads.decrementAndGet();
}
}
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue) {
@Override
public void execute(Runnable command) {
super.execute(command);
if (getQueue().size() > waitingThreads.get()) super.execute(SENTINEL_NO_OP);
}
};
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (r == SENTINEL_NO_OP) return;
else throw new RejectedExecutionException();
}
});
私が考えることができる最良の解決策は、拡張することです。
ThreadPoolExecutor
には、beforeExecute
およびafterExecute
といういくつかのフックメソッドがあります。拡張機能では、タスクをフィードするために境界キューを使用し、オーバーフローを処理するために2番目の非境界キューを使用し続けることができます。誰かがsubmit
を呼び出すと、制限されたキューにリクエストを配置することができます。例外が発生した場合は、タスクをオーバーフローキューに入れるだけです。次に、afterExecute
フックを使用して、タスクの終了後にオーバーフローキューに何かがあるかどうかを確認できます。このようにして、エグゼキュータはまずその制限されたキューの内容を処理し、時間が許せばこの無制限のキューから自動的にプルします。
あなたのソリューションよりも多くの作業のように見えますが、少なくともキューに予期しない動作を与えることは含まれていません。また、スローにかなり時間がかかる例外に依存するのではなく、キューとスレッドのステータスをチェックするより良い方法があると思います。