ThreadPoolExecutor
最大サイズに達してキューがいっぱいになると、新しいタスクを追加しようとするとsubmit()
メソッドがブロックするような方法を作成したいと思います。RejectedExecutionHandler
そのためのカスタムを実装する必要がありますか、それとも標準のJavaライブラリを使用してこれを行う既存の方法はありますか?
ThreadPoolExecutor
最大サイズに達してキューがいっぱいになると、新しいタスクを追加しようとするとsubmit()
メソッドがブロックするような方法を作成したいと思います。RejectedExecutionHandler
そのためのカスタムを実装する必要がありますか、それとも標準のJavaライブラリを使用してこれを行う既存の方法はありますか?
回答:
私が見つけた可能な解決策の1つ:
public class BoundedExecutor {
private final Executor exec;
private final Semaphore semaphore;
public BoundedExecutor(Executor exec, int bound) {
this.exec = exec;
this.semaphore = new Semaphore(bound);
}
public void submitTask(final Runnable command)
throws InterruptedException, RejectedExecutionException {
semaphore.acquire();
try {
exec.execute(new Runnable() {
public void run() {
try {
command.run();
} finally {
semaphore.release();
}
}
});
} catch (RejectedExecutionException e) {
semaphore.release();
throw e;
}
}
}
他の解決策はありますか?RejectedExecutionHandler
そのような状況を処理するための標準的な方法のように思われるので、私はに基づいた何かを好みます。
throw e;
ある、NOT本に。JCIPは正解です。
ThreadPoolExecutorとブロッキングキューを使用できます。
public class ImageManager {
BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(blockQueueSize);
RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.CallerRunsPolicy();
private ExecutorService executorService = new ThreadPoolExecutor(numOfThread, numOfThread,
0L, TimeUnit.MILLISECONDS, blockingQueue, rejectedExecutionHandler);
private int downloadThumbnail(String fileListPath){
executorService.submit(new yourRunnable());
}
}
CallerRunsPolicy
呼び出しスレッドで拒否されたタスクを実行するを使用する必要があります。この方法では、そのタスクが完了するまで新しいタスクをエグゼキューターに送信できません。そのタスクが完了すると、いくつかの空きプールスレッドが存在するか、プロセスが繰り返されます。
ドキュメントから:
拒否されたタスク
メソッドexecute(java.lang.Runnable)で送信された新しいタスクは、エグゼキュータがシャットダウンされたとき、およびエグゼキュータが最大スレッドとワークキュー容量の両方に有限の境界を使用し、飽和したときに拒否されます。どちらの場合も、executeメソッドは、RejectedExecutionHandlerのRejectedExecutionHandler.rejectedExecution(java.lang.Runnable、java.util.concurrent.ThreadPoolExecutor)メソッドを呼び出します。4つの定義済みハンドラーポリシーが提供されます。
- デフォルトのThreadPoolExecutor.AbortPolicyでは、ハンドラーは拒否時にランタイムRejectedExecutionExceptionをスローします。
- ThreadPoolExecutor.CallerRunsPolicyでは、execute自体を呼び出すスレッドがタスクを実行します。これにより、新しいタスクが送信される速度を遅くする単純なフィードバック制御メカニズムが提供されます。
- ThreadPoolExecutor.DiscardPolicyでは、実行できないタスクは単に削除されます。
- ThreadPoolExecutor.DiscardOldestPolicyで、エグゼキュータがシャットダウンされていない場合、作業キューの先頭にあるタスクが削除され、実行が再試行されます(これが再び失敗し、これが繰り返される可能性があります)。
また、ThreadPoolExecutor
コンストラクターを呼び出すときは、必ずArrayBlockingQueueなどの境界キューを使用してください。そうでなければ、何も拒否されません。
編集:コメントに応じて、ArrayBlockingQueueのサイズをスレッドプールの最大サイズと等しくなるように設定し、AbortPolicyを使用します。
編集2:わかりました、私はあなたが何を手に入れているのかわかります。これについてはどうですか?beforeExecute()
メソッドをオーバーライドして、をgetActiveCount()
超えていないことを確認し、超えgetMaximumPoolSize()
ている場合は、スリープしてから再試行しますか?
HibernateにBlockPolicy
はシンプルながあり、必要な処理を実行できます。
/**
* A handler for rejected tasks that will have the caller block until
* space is available.
*/
public static class BlockPolicy implements RejectedExecutionHandler {
/**
* Creates a <tt>BlockPolicy</tt>.
*/
public BlockPolicy() { }
/**
* Puts the Runnable to the blocking queue, effectively blocking
* the delegating thread until space is available.
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
try {
e.getQueue().put( r );
}
catch (InterruptedException e1) {
log.error( "Work discarded, thread was interrupted while waiting for space to schedule: {}", r );
}
}
}
ThreadPoolExecutor
は文字通り、「メソッドgetQueue()を使用すると、監視とデバッグのためにワークキューにアクセスできます。このメソッドを他の目的で使用することはお勧めしません。」これが広く知られているライブラリで利用できることは、本当に悲しいことです。
BoundedExecutor
上に引用された答えの練習でのJava並行処理を使用すると、エグゼキュータのためにアンバウンド形式のキューを使用する場合にのみ正しく機能、または拘束セマフォは、キューのサイズよりも大きくありません。セマフォはサブミットスレッドとプール内のスレッド間で共有される状態であり、キューサイズ<バインド<=(キューサイズ+プールサイズ)の場合でもエグゼキュータを飽和させることができます。
使用CallerRunsPolicy
は、タスクが永久に実行されない場合にのみ有効です。その場合、送信スレッドはrejectedExecution
永久に残ります。送信スレッドは新しいタスクを送信できないため、タスクの実行に長い時間がかかる場合はお勧めできません。それ自体がタスクを実行している場合は、他のことを行います。
それが許容できない場合は、タスクを送信する前に、エグゼキュータの境界キューのサイズを確認することをお勧めします。キューがいっぱいの場合は、しばらく待ってから、送信を再試行してください。スループットは低下しますが、他の多くの提案されたソリューションよりも単純なソリューションであり、タスクが拒否されないことが保証されます。
私は知っている、それはハックですが、私の意見では、ここで提供されたものの中で最もクリーンなハックです;-)
ThreadPoolExecutorは「put」ではなく「offer」のブロッキングキューを使用するため、ブロッキングキューの「offer」の動作をオーバーライドします。
class BlockingQueueHack<T> extends ArrayBlockingQueue<T> {
BlockingQueueHack(int size) {
super(size);
}
public boolean offer(T task) {
try {
this.put(task);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
}
}
ThreadPoolExecutor tp = new ThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new BlockingQueueHack(5));
私はそれをテストしました、そしてそれはうまくいくようです。タイムアウトポリシーの実装は、読者の演習として残しておきます。
次のクラスはThreadPoolExecutorをラップし、セマフォを使用してブロックし、作業キューがいっぱいになります。
public final class BlockingExecutor {
private final Executor executor;
private final Semaphore semaphore;
public BlockingExecutor(int queueSize, int corePoolSize, int maxPoolSize, int keepAliveTime, TimeUnit unit, ThreadFactory factory) {
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
this.executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, queue, factory);
this.semaphore = new Semaphore(queueSize + maxPoolSize);
}
private void execImpl (final Runnable command) throws InterruptedException {
semaphore.acquire();
try {
executor.execute(new Runnable() {
@Override
public void run() {
try {
command.run();
} finally {
semaphore.release();
}
}
});
} catch (RejectedExecutionException e) {
// will never be thrown with an unbounded buffer (LinkedBlockingQueue)
semaphore.release();
throw e;
}
}
public void execute (Runnable command) throws InterruptedException {
execImpl(command);
}
}
このラッパークラスは、Brian Goetz著のJava Concurrency in Practiceで提供されているソリューションに基づいています。本のソリューションはExecutor
、セマフォに使用されるan とboundの2つのコンストラクタパラメータのみを受け取ります。これは、Fixpointの回答に示されています。そのアプローチには問題があります。プールスレッドがビジーで、キューがいっぱいですが、セマフォが許可を解放したばかりの状態になる可能性があります。(semaphore.release()
finallyブロック内)。この状態では、新しいタスクは解放されたばかりの許可を取得できますが、タスクキューがいっぱいであるため拒否されます。もちろん、これはあなたが望むものではありません。この場合はブロックします。
これを解決するには、JCiPが明確に述べているように、無制限のキューを使用する必要があります。セマフォはガードとして機能し、仮想キューサイズの効果を与えます。これには、ユニットにmaxPoolSize + virtualQueueSize + maxPoolSize
タスクを含めることができるという副作用があります。何故ですか?semaphore.release()
finallyブロック内にあるためです
。すべてのプールスレッドがこのステートメントを同時に呼び出すと、maxPoolSize
許可が解放され、同じ数のタスクがユニットに入ることができます。制限付きキューを使用している場合でも、キューがいっぱいになり、タスクが拒否されます。これは、プールスレッドがほぼ終了したときにのみ発生することがわかっているため、問題ありません。プールスレッドはブロックされないことがわかっているため、タスクはすぐにキューから取得されます。
ただし、制限付きキューを使用できます。ちょうどそのサイズが等しいことを確認してくださいvirtualQueueSize + maxPoolSize
。大きいサイズは役に立たない、セマフォはより多くのアイテムを入れないようにします。小さいサイズはタスクが拒否される結果になります。サイズが小さくなると、タスクが拒否される可能性が高くなります。たとえば、maxPoolSize = 2とvirtualQueueSize = 5の制限付きエグゼキューターが必要だとします。次に、セマフォを5 + 2 = 7の許可と5 + 2 = 7の実際のキューサイズで取得します。ユニットに含めることができるタスクの実際の数は、2 + 5 + 2 = 9になります。エグゼキュータがいっぱい(キューに5つのタスク、スレッドプールに2つのタスクがあるため、0のパーミットが利用可能)で、すべてのプールスレッドがパーミットを解放すると、入ってくるタスクによって2つのパーミットを取得できます。
現在、JCiPのソリューションは、これらすべての制約(無制限のキュー、またはこれらの数学の制限などで制限されている)を適用しないため、使用するのがやや面倒です。これは、すでに利用可能なパーツに基づいて新しいスレッドセーフクラスを構築する方法を示す良い例にすぎないと思いますが、完全に再利用可能なクラスではありません。後者が著者の意図であったとは思いません。
このようなカスタムRejectedExecutionHandlerを使用できます
ThreadPoolExecutor tp= new ThreadPoolExecutor(core_size, // core size
max_handlers, // max size
timeout_in_seconds, // idle timeout
TimeUnit.SECONDS, queue, new RejectedExecutionHandler() {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// This will block if the queue is full
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
System.err.println(e.getMessage());
}
}
});
エグゼキューターが使用する独自のブロッキングキューを作成し、必要なブロッキング動作を使用しながら、常に利用可能な残りの容量を返します(エグゼキューターがコアプールより多くのスレッドを作成したり、拒否ハンドラーをトリガーしたりしないようにします)。
私はこれがあなたが探しているブロッキング行動を手に入れると信じています。拒否ハンドラは、実行者がタスクを実行できないことを示すため、請求書に適合しません。私が想像できることは、ハンドラーでなんらかの形の「ビジー待機」が発生することです。それはあなたが望むものではありません、あなたは発信者をブロックするエグゼキューターのためのキューが欲しいです...
ThreadPoolExecutor
offer
メソッドを使用してタスクをキューに追加します。BlockingQueue
でブロックするカスタムを作成すると、契約offer
が破られBlockingQueue
ます。
ThreadPoolExecutor
使用するように実装された理由はありますか?また、クライアントコードがどのコードをいつ使用するかを指示する方法があった場合、カスタムソリューションをハンドロールしようとする多くの人々は安心しましたoffer
put
最近、この問題に同じ問題があることがわかりました。OPは明示的に言っていませんがRejectedExecutionHandler
、サブミッターのスレッドでタスクを実行するを使用したくありません。これは、このタスクが長時間実行されている場合、ワーカースレッドの使用率が低下するためです。
すべての回答とコメント、特にセマフォの問題のあるソリューションを使用するか、afterExecute
私はThreadPoolExecutorのコードを詳しく調べて、解決策があるかどうかを確認しました。2000行以上の(コメントされた)コードがあり、その一部がめまいを感じさせることに驚いていました。私が実際に持っているかなり単純な要件を考えると、1つのプロデューサーと複数のコンシューマーがあり、コンシューマーが作業を実行できないときにプロデューサーをブロックさせます-私は自分のソリューションを導入することにしました。それはではなくExecutorService
単なるExecutor
です。また、スレッド数を作業負荷に適合させませんが、固定数のスレッドのみを保持します。これも私の要件に適合します。これがコードです。それについて遠慮なく怒鳴ってください:-)
package x;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
/**
* distributes {@code Runnable}s to a fixed number of threads. To keep the
* code lean, this is not an {@code ExecutorService}. In particular there is
* only very simple support to shut this executor down.
*/
public class ParallelExecutor implements Executor {
// other bounded queues work as well and are useful to buffer peak loads
private final BlockingQueue<Runnable> workQueue =
new SynchronousQueue<Runnable>();
private final Thread[] threads;
/*+**********************************************************************/
/**
* creates the requested number of threads and starts them to wait for
* incoming work
*/
public ParallelExecutor(int numThreads) {
this.threads = new Thread[numThreads];
for(int i=0; i<numThreads; i++) {
// could reuse the same Runner all over, but keep it simple
Thread t = new Thread(new Runner());
this.threads[i] = t;
t.start();
}
}
/*+**********************************************************************/
/**
* returns immediately without waiting for the task to be finished, but may
* block if all worker threads are busy.
*
* @throws RejectedExecutionException if we got interrupted while waiting
* for a free worker
*/
@Override
public void execute(Runnable task) {
try {
workQueue.put(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RejectedExecutionException("interrupt while waiting for a free "
+ "worker.", e);
}
}
/*+**********************************************************************/
/**
* Interrupts all workers and joins them. Tasks susceptible to an interrupt
* will preempt their work. Blocks until the last thread surrendered.
*/
public void interruptAndJoinAll() throws InterruptedException {
for(Thread t : threads) {
t.interrupt();
}
for(Thread t : threads) {
t.join();
}
}
/*+**********************************************************************/
private final class Runner implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
Runnable task;
try {
task = workQueue.take();
} catch (InterruptedException e) {
// canonical handling despite exiting right away
Thread.currentThread().interrupt();
return;
}
try {
task.run();
} catch (RuntimeException e) {
// production code to use a logging framework
e.printStackTrace();
}
}
}
}
}
のjava.util.concurrent.Semaphore
動作を使用および委任することにより、この問題を解決する非常にエレガントな方法があると思いますExecutor.newFixedThreadPool
。新しいエグゼキューターサービスは、実行するスレッドがある場合にのみ新しいタスクを実行します。ブロッキングはセマフォによって管理され、許可の数はスレッドの数と同じです。タスクが完了すると、タスクは許可を返します。
public class FixedThreadBlockingExecutorService extends AbstractExecutorService {
private final ExecutorService executor;
private final Semaphore blockExecution;
public FixedThreadBlockingExecutorService(int nTreads) {
this.executor = Executors.newFixedThreadPool(nTreads);
blockExecution = new Semaphore(nTreads);
}
@Override
public void shutdown() {
executor.shutdown();
}
@Override
public List<Runnable> shutdownNow() {
return executor.shutdownNow();
}
@Override
public boolean isShutdown() {
return executor.isShutdown();
}
@Override
public boolean isTerminated() {
return executor.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return executor.awaitTermination(timeout, unit);
}
@Override
public void execute(Runnable command) {
blockExecution.acquireUninterruptibly();
executor.execute(() -> {
try {
command.run();
} finally {
blockExecution.release();
}
});
}
過去にも同じニーズがありました。共有スレッドプールに支えられた、各クライアントのサイズが固定された一種のブロッキングキューです。私は自分の種類のThreadPoolExecutorを作成することになりました。
UserThreadPoolExecutor(ブロッキングキュー(クライアントごと)+スレッドプール(すべてのクライアント間で共有))
参照:https : //github.com/d4rxh4wx/UserThreadPoolExecutor
各UserThreadPoolExecutorには、共有ThreadPoolExecutorからスレッドの最大数が与えられます
各UserThreadPoolExecutorは次のことができます。
この拒否ポリシーはエラスティック検索クライアントで見つかりました。ブロッキングキューで呼び出し元のスレッドをブロックします。以下のコード-
static class ForceQueuePolicy implements XRejectedExecutionHandler
{
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
{
try
{
executor.getQueue().put(r);
}
catch (InterruptedException e)
{
//should never happen since we never wait
throw new EsRejectedExecutionException(e);
}
}
@Override
public long rejected()
{
return 0;
}
}
私は最近、しかし上、類似した何かを達成する必要がありましたScheduledExecutorService
。
また、メソッドに渡される遅延を処理し、呼び出し元が予期したとおりにタスクが実行のために送信されるか、単に失敗してをスローするようにする必要もありますRejectedExecutionException
。
ScheduledThreadPoolExecutor
タスクを実行またはサブミットする他のメソッドは内部的に呼び出さ#schedule
れ、オーバーライドされたメソッドを呼び出します。
import java.util.concurrent.*;
public class BlockingScheduler extends ScheduledThreadPoolExecutor {
private final Semaphore maxQueueSize;
public BlockingScheduler(int corePoolSize,
ThreadFactory threadFactory,
int maxQueueSize) {
super(corePoolSize, threadFactory, new AbortPolicy());
this.maxQueueSize = new Semaphore(maxQueueSize);
}
@Override
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
final long newDelayInMs = beforeSchedule(command, unit.toMillis(delay));
return super.schedule(command, newDelayInMs, TimeUnit.MILLISECONDS);
}
@Override
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay,
TimeUnit unit) {
final long newDelayInMs = beforeSchedule(callable, unit.toMillis(delay));
return super.schedule(callable, newDelayInMs, TimeUnit.MILLISECONDS);
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
return super.scheduleAtFixedRate(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
return super.scheduleWithFixedDelay(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
}
@Override
protected void afterExecute(Runnable runnable, Throwable t) {
super.afterExecute(runnable, t);
try {
if (t == null && runnable instanceof Future<?>) {
try {
((Future<?>) runnable).get();
} catch (CancellationException | ExecutionException e) {
t = e;
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null) {
System.err.println(t);
}
} finally {
releaseQueueUsage();
}
}
private long beforeSchedule(Runnable runnable, long delay) {
try {
return getQueuePermitAndModifiedDelay(delay);
} catch (InterruptedException e) {
getRejectedExecutionHandler().rejectedExecution(runnable, this);
return 0;
}
}
private long beforeSchedule(Callable callable, long delay) {
try {
return getQueuePermitAndModifiedDelay(delay);
} catch (InterruptedException e) {
getRejectedExecutionHandler().rejectedExecution(new FutureTask(callable), this);
return 0;
}
}
private long getQueuePermitAndModifiedDelay(long delay) throws InterruptedException {
final long beforeAcquireTimeStamp = System.currentTimeMillis();
maxQueueSize.tryAcquire(delay, TimeUnit.MILLISECONDS);
final long afterAcquireTimeStamp = System.currentTimeMillis();
return afterAcquireTimeStamp - beforeAcquireTimeStamp;
}
private void releaseQueueUsage() {
maxQueueSize.release();
}
}
私はここにコードを持っています、フィードバックがあれば感謝します。 https://github.com/AmitabhAwasthi/BlockingScheduler
これは本当にうまくいくように見える解決策です。それはNotifyingBlockingThreadPoolExecutorと呼ばれます。
編集:このコードには問題があり、await()メソッドはバグがあります。shutdown()+ awaitTermination()の呼び出しは正常に動作するようです。
特に、拒否されたタスクが「キューをスキップ」して、以前に送信されたタスクの前に実行されるのを許可するため、私は常にCallerRunsPolicyを好きではありません。さらに、呼び出しスレッドでタスクを実行すると、最初のスロットが利用可能になるのを待つよりもはるかに時間がかかる場合があります。
カスタムのRejectedExecutionHandlerを使用してこの問題を解決しました。これは、呼び出しスレッドをしばらくブロックし、タスクを再度送信しようとするだけです。
public class BlockWhenQueueFull implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// The pool is full. Wait, then try again.
try {
long waitMs = 250;
Thread.sleep(waitMs);
} catch (InterruptedException interruptedException) {}
executor.execute(r);
}
}
このクラスは、他のようにスレッドプールエグゼキューターでRejectedExecutinHandlerとして使用できます。次に例を示します。
executorPool = new ThreadPoolExecutor(1, 1, 10,
TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
new BlockWhenQueueFull());
私が目にする唯一の欠点は、呼び出し側のスレッドが厳密に必要なものよりも少し長くロックされる可能性があることです(最大250ミリ秒)。さらに、このエグゼキューターは効果的に再帰的に呼び出されているため、スレッドが使用可能になるまでに非常に長い時間(時間)待機すると、スタックオーバーフローが発生する可能性があります。
それにもかかわらず、私は個人的にこの方法が好きです。コンパクトで理解しやすく、うまく機能します。