ThreadPoolExecutorを使用して多くのタスクを実行しようとしています。以下は架空の例です。
def workQueue = new ArrayBlockingQueue<Runnable>(3, false)
def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue)
for(int i = 0; i < 100000; i++)
threadPoolExecutor.execute(runnable)
問題は、すぐにJava.util.concurrent.RejectedExecutionException
タスクの数が作業キューのサイズを超えているため。ただし、私が探している望ましい動作は、キューに空きができるまでメインスレッドをブロックすることです。これを達成する最良の方法は何ですか?
非常に狭い状況では、必要なことを行うJava.util.concurrent.RejectedExecutionHandlerを実装できます。
RejectedExecutionHandler block = new RejectedExecutionHandler() {
rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
executor.getQueue().put( r );
}
};
ThreadPoolExecutor pool = new ...
pool.setRejectedExecutionHandler(block);
今。これは非常に悪い考え以下の理由によります
ほぼ常に優れた戦略は、execute()を呼び出しているスレッドでタスクを実行することでアプリを調整するThreadPoolExecutor.CallerRunsPolicyをインストールすることです。
ただし、固有のリスクをすべて備えたブロッキング戦略が本当に必要な場合もあります。これらの条件下で言う
だから、私が言うように。それはめったに必要ではなく、危険な場合がありますが、そこに行きます。
幸運を。
semaphore
を使用して、スレッドがプールに入るのをブロックできます。
ExecutorService service = new ThreadPoolExecutor(
3,
3,
1,
TimeUnit.HOURS,
new ArrayBlockingQueue<>(6, false)
);
Semaphore lock = new Semaphore(6); // equal to queue capacity
for (int i = 0; i < 100000; i++ ) {
try {
lock.acquire();
service.submit(() -> {
try {
task.run();
} finally {
lock.release();
}
});
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
いくつかの落とし穴:
キューのサイズは、コアスレッドの数よりも大きくする必要があります。キューサイズを3にすると、最終的には次のようになります。
上記の例は、メインスレッドblockingスレッド1のスレッドに変換されます。小さな期間のように思えるかもしれませんが、現在は頻度を日と月で乗算します。突然の短い期間のすべてが、膨大な時間の浪費につながります。
する必要があるのは、ThreadPoolExecutorをExecutorにラップして、その中で同時に実行される操作の量を明示的に制限することです。
private static class BlockingExecutor implements Executor {
final Semaphore semaphore;
final Executor delegate;
private BlockingExecutor(final int concurrentTasksLimit, final Executor delegate) {
semaphore = new Semaphore(concurrentTasksLimit);
this.delegate = delegate;
}
@Override
public void execute(final Runnable command) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
final Runnable wrapped = () -> {
try {
command.run();
} finally {
semaphore.release();
}
};
delegate.execute(wrapped);
}
}
ConcurrentTasksLimitをデリゲートエグゼキューターのthreadPoolSize + queueSizeに調整すると、問題をほぼ解決できます。
この場合のコードスニペットは次のとおりです。
public void executeBlocking( Runnable command ) {
if ( threadPool == null ) {
logger.error( "Thread pool '{}' not initialized.", threadPoolName );
return;
}
ThreadPool threadPoolMonitor = this;
boolean accepted = false;
do {
try {
threadPool.execute( new Runnable() {
@Override
public void run() {
try {
command.run();
}
// to make sure that the monitor is freed on exit
finally {
// Notify all the threads waiting for the resource, if any.
synchronized ( threadPoolMonitor ) {
threadPoolMonitor.notifyAll();
}
}
}
} );
accepted = true;
}
catch ( RejectedExecutionException e ) {
// Thread pool is full
try {
// Block until one of the threads finishes its job and exits.
synchronized ( threadPoolMonitor ) {
threadPoolMonitor.wait();
}
}
catch ( InterruptedException ignored ) {
// return immediately
break;
}
}
} while ( !accepted );
}
threadPoolは、既に初期化されているJava.util.concurrent.ExecutorServiceのローカルインスタンスです。
これは私がやったことです:
int NUM_THREADS = 6;
Semaphore lock = new Semaphore(NUM_THREADS);
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 100000; i++) {
try {
lock.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.execute(() -> {
try {
// Task logic
} finally {
lock.release();
}
});
}
カスタム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);
}
}
このクラスは、スレッドプールエグゼキューターで、他のRejectedExecutionHandlerとして使用できます。この例では:
executorPool = new def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue, new BlockWhenQueueFull())
私が見る唯一の欠点は、呼び出しスレッドが厳密に必要な時間よりもわずかに長くロックされる可能性があることです(最大250ミリ秒)。多くの短時間実行タスクでは、おそらく待機時間を10ミリ秒程度に減らします。さらに、このエグゼキューターは事実上再帰的に呼び出されるため、スレッドが使用可能になるまでの非常に長い待機(時間)により、スタックオーバーフローが発生する可能性があります。
それにもかかわらず、私は個人的にこの方法が好きです。コンパクトで理解しやすく、うまく機能します。重要なものがありませんか?