単一のスレッドが並行して実行できるI/O集約型のタスクを生成するソリューションをコーディングしようとしています。各タスクには重要なメモリ内データがあります。そのため、現在保留中のタスクの数を制限できるようにします。
次のようにThreadPoolExecutorを作成した場合:
_ ThreadPoolExecutor executor = new ThreadPoolExecutor(numWorkerThreads, numWorkerThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(maxQueue));
_
その後、キューがいっぱいになり、すべてのスレッドがすでにビジー状態になると、executor.submit(callable)
はRejectedExecutionException
をスローします。
キューがいっぱいですべてのスレッドがビジーのときにexecutor.submit(callable)
ブロックを作成するにはどうすればよいですか?
[〜#〜] edit [〜#〜]:試しました this :
_executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
_
そして、それは私が達成したい効果を多少達成しますが、洗練されていません(基本的に拒否されたスレッドは呼び出しスレッドで実行されるため、呼び出しスレッドがそれ以上送信するのをブロックします)。
編集:(質問してから5年後)
この質問とその答えを読んでいる人には、受け入れられた答えを正しい解決策として受け取らないでください。すべての回答とコメントをお読みください。
私もこれと同じことをしました。秘Theは、offer()メソッドが実際にput()であるBlockingQueueを作成することです。 (必要な基本BlockingQueue implを使用できます)。
public class LimitedQueue<E> extends LinkedBlockingQueue<E>
{
public LimitedQueue(int maxSize)
{
super(maxSize);
}
@Override
public boolean offer(E e)
{
// turn offer() and add() into a blocking calls (unless interrupted)
try {
put(e);
return true;
} catch(InterruptedException ie) {
Thread.currentThread().interrupt();
}
return false;
}
}
これは、corePoolSize==maxPoolSize
そのため、注意が必要です(コメントを参照)。
これが私の終わりにどのように解決したかです:
(注:このソリューションは、Callableを送信するスレッドをブロックするため、RejectedExecutionExceptionがスローされるのを防ぎます)
public class BoundedExecutor extends ThreadPoolExecutor{
private final Semaphore semaphore;
public BoundedExecutor(int bound) {
super(bound, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
semaphore = new Semaphore(bound);
}
/**Submits task to execution pool, but blocks while number of running threads
* has reached the bound limit
*/
public <T> Future<T> submitButBlockIfFull(final Callable<T> task) throws InterruptedException{
semaphore.acquire();
return submit(task);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
semaphore.release();
}
}
現在受け入れられている答えには潜在的に重大な問題があります-corePoolSize < maxPoolSize
がある場合、ThreadPoolExecutor.executeの動作が変更され、ThreadPoolExecutorロジックはコアを超えて追加のワーカーを追加しません。
ThreadPoolExecutor 。execute(Runnable)から:
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
具体的には、最後の「else」ブロックがヒットすることはありません。
より良い代替策は、OPがすでに行っていることと同様のことを行うことです。同じput
ロジックを行うには、 RejectedExecutionHandler を使用します。
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
if (!executor.isShutdown()) {
executor.getQueue().put(r);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RejectedExecutionException("Executor was interrupted while the task was waiting to put on work queue", e);
}
}
コメントで指摘されているように、このアプローチには注意すべき点がいくつかあります( this answer を参照):
corePoolSize==0
の場合、タスクが表示される前にプール内のすべてのスレッドが停止する競合状態がありますThreadPoolExecutor
には適用されない)を使用すると、ハンドラーも同じ方法でラップしない限り、問題が発生します。これらの落とし穴を念頭に置いて、このソリューションはほとんどの典型的なThreadPoolExecutorsで機能し、corePoolSize < maxPoolSize
の場合を適切に処理します。
同様の問題があり、ThreadPoolExecutor
のbeforeExecute/afterExecute
フックを使用して実装しました。
import Java.util.concurrent.BlockingQueue;
import Java.util.concurrent.ThreadPoolExecutor;
import Java.util.concurrent.TimeUnit;
import Java.util.concurrent.locks.Condition;
import Java.util.concurrent.locks.ReentrantLock;
/**
* Blocks current task execution if there is not enough resources for it.
* Maximum task count usage controlled by maxTaskCount property.
*/
public class BlockingThreadPoolExecutor extends ThreadPoolExecutor {
private final ReentrantLock taskLock = new ReentrantLock();
private final Condition unpaused = taskLock.newCondition();
private final int maxTaskCount;
private volatile int currentTaskCount;
public BlockingThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, int maxTaskCount) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
this.maxTaskCount = maxTaskCount;
}
/**
* Executes task if there is enough system resources for it. Otherwise
* waits.
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
taskLock.lock();
try {
// Spin while we will not have enough capacity for this job
while (maxTaskCount < currentTaskCount) {
try {
unpaused.await();
} catch (InterruptedException e) {
t.interrupt();
}
}
currentTaskCount++;
} finally {
taskLock.unlock();
}
}
/**
* Signalling that one more task is welcome
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
taskLock.lock();
try {
currentTaskCount--;
unpaused.signalAll();
} finally {
taskLock.unlock();
}
}
}
これで十分です。ところで、元の実装はタスクサイズに基づいていました。1つのタスクが別のタスクよりも100倍大きくなり、2つの巨大なタスクを送信することで問題が解決しましたが、1つの大小を実行しても大丈夫でした。 I/Oを集中的に使用するタスクがほぼ同じサイズである場合、このクラスを使用できます。それ以外の場合はお知らせください。サイズベースの実装を投稿します。
追伸ThreadPoolExecutor
javadocを確認する必要があります。 Doug Leaの簡単にカスタマイズできる方法についての本当に素晴らしいユーザーガイドです。
これは古い質問ですが、新しいタスクの作成が非常に高速で、既存のタスクが十分に速く完了しなかったためにOutOfMemoryErrorが発生しすぎるという同様の問題がありました。
私の場合、Callables
が送信され、結果が必要なので、executor.submit()
によって返されるFutures
をすべて保存する必要があります。私の解決策は、Futures
を最大サイズのBlockingQueue
に入れることでした。そのキューがいっぱいになると、いくつかのタスクが完了するまで(キューから要素が削除されるまで)タスクは生成されません。擬似コード内:
final ExecutorService executor = Executors.newFixedThreadPool(numWorkerThreads);
final LinkedBlockingQueue<Future> futures = new LinkedBlockingQueue<>(maxQueueSize);
try {
Thread taskGenerator = new Thread() {
@Override
public void run() {
while (reader.hasNext) {
Callable task = generateTask(reader.next());
Future future = executor.submit(task);
try {
// if queue is full blocks until a task
// is completed and hence no future tasks are submitted.
futures.put(compoundFuture);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
executor.shutdown();
}
}
taskGenerator.start();
// read from queue as long as task are being generated
// or while Queue has elements in it
while (taskGenerator.isAlive()
|| !futures.isEmpty()) {
Future compoundFuture = futures.take();
// do something
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
} catch (ExecutionException ex) {
throw new MyException(ex);
} finally {
executor.shutdownNow();
}
デコレータパターンに従い、セマフォを使用して実行タスクの数を制御するソリューションを実装しました。任意のExecutor
および以下で使用できます。
RejectedExecutionException
がスローされます)import static Java.util.concurrent.TimeUnit.MILLISECONDS;
import Java.time.Duration;
import Java.util.Objects;
import Java.util.concurrent.Executor;
import Java.util.concurrent.RejectedExecutionException;
import Java.util.concurrent.Semaphore;
import javax.annotation.Nonnull;
public class BlockingOnFullQueueExecutorDecorator implements Executor {
private static final class PermitReleasingDecorator implements Runnable {
@Nonnull
private final Runnable delegate;
@Nonnull
private final Semaphore semaphore;
private PermitReleasingDecorator(@Nonnull final Runnable task, @Nonnull final Semaphore semaphoreToRelease) {
this.delegate = task;
this.semaphore = semaphoreToRelease;
}
@Override
public void run() {
try {
this.delegate.run();
}
finally {
// however execution goes, release permit for next task
this.semaphore.release();
}
}
@Override
public final String toString() {
return String.format("%s[delegate='%s']", getClass().getSimpleName(), this.delegate);
}
}
@Nonnull
private final Semaphore taskLimit;
@Nonnull
private final Duration timeout;
@Nonnull
private final Executor delegate;
public BlockingOnFullQueueExecutorDecorator(@Nonnull final Executor executor, final int maximumTaskNumber, @Nonnull final Duration maximumTimeout) {
this.delegate = Objects.requireNonNull(executor, "'executor' must not be null");
if (maximumTaskNumber < 1) {
throw new IllegalArgumentException(String.format("At least one task must be permitted, not '%d'", maximumTaskNumber));
}
this.timeout = Objects.requireNonNull(maximumTimeout, "'maximumTimeout' must not be null");
if (this.timeout.isNegative()) {
throw new IllegalArgumentException("'maximumTimeout' must not be negative");
}
this.taskLimit = new Semaphore(maximumTaskNumber);
}
@Override
public final void execute(final Runnable command) {
Objects.requireNonNull(command, "'command' must not be null");
try {
// attempt to acquire permit for task execution
if (!this.taskLimit.tryAcquire(this.timeout.toMillis(), MILLISECONDS)) {
throw new RejectedExecutionException(String.format("Executor '%s' busy", this.delegate));
}
}
catch (final InterruptedException e) {
// restore interrupt status
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
this.delegate.execute(new PermitReleasingDecorator(command, this.taskLimit));
}
@Override
public final String toString() {
return String.format("%s[availablePermits='%s',timeout='%s',delegate='%s']", getClass().getSimpleName(), this.taskLimit.availablePermits(),
this.timeout, this.delegate);
}
}
ArrayBlockingQueue
の代わりにLinkedBlockingQueue
を使用するのと同じくらい簡単だと思います。
私を無視してください...それは全く間違っています。 ThreadPoolExecutor
はput
ではなく_Queue#offer
_を呼び出しますが、これは必要な効果をもたらします。
ThreadPoolExecutor
を拡張し、put
の代わりにoffer
を呼び出すexecute(Runnable)
の実装を提供できます。
それは私が恐れている完全に満足のいく答えのようには見えません。