Sto riscontrando un problema in cui se provo a ridimensionare la ThreadPoolExecutor
dimensione del pool principale di un numero diverso dopo la creazione del pool, quindi a intermittenza, alcune attività vengono rifiutate con un RejectedExecutionException
anche se non invio mai più del queueSize + maxPoolSize
numero di attività.
Il problema che sto cercando di risolvere è l'estensione ThreadPoolExecutor
che ridimensiona i thread principali in base alle esecuzioni in sospeso che si trovano nella coda del pool di thread. Ne ho bisogno perché di default a ThreadPoolExecutor
creerà un nuovo Thread
solo se la coda è piena.
Ecco un piccolo programma autonomo Pure Java 8 che dimostra il problema.
import static java.lang.Math.max;
import static java.lang.Math.min;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolResizeTest {
public static void main(String[] args) throws Exception {
// increase the number of iterations if unable to reproduce
// for me 100 iterations have been enough
int numberOfExecutions = 100;
for (int i = 1; i <= numberOfExecutions; i++) {
executeOnce();
}
}
private static void executeOnce() throws Exception {
int minThreads = 1;
int maxThreads = 5;
int queueCapacity = 10;
ThreadPoolExecutor pool = new ThreadPoolExecutor(
minThreads, maxThreads,
0, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(queueCapacity),
new ThreadPoolExecutor.AbortPolicy()
);
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> resizeThreadPool(pool, minThreads, maxThreads),
0, 10, TimeUnit.MILLISECONDS);
CompletableFuture<Void> taskBlocker = new CompletableFuture<>();
try {
int totalTasksToSubmit = queueCapacity + maxThreads;
for (int i = 1; i <= totalTasksToSubmit; i++) {
// following line sometimes throws a RejectedExecutionException
pool.submit(() -> {
// block the thread and prevent it from completing the task
taskBlocker.join();
});
// Thread.sleep(10); //enabling even a small sleep makes the problem go away
}
} finally {
taskBlocker.complete(null);
scheduler.shutdown();
pool.shutdown();
}
}
/**
* Resize the thread pool if the number of pending tasks are non-zero.
*/
private static void resizeThreadPool(ThreadPoolExecutor pool, int minThreads, int maxThreads) {
int pendingExecutions = pool.getQueue().size();
int approximateRunningExecutions = pool.getActiveCount();
/*
* New core thread count should be the sum of pending and currently executing tasks
* with an upper bound of maxThreads and a lower bound of minThreads.
*/
int newThreadCount = min(maxThreads, max(minThreads, pendingExecutions + approximateRunningExecutions));
pool.setCorePoolSize(newThreadCount);
pool.prestartAllCoreThreads();
}
}
Perché il pool dovrebbe mai lanciare un RejectedExecutionException se non invio mai più di queueCapacity + maxThreads. Non sto mai cambiando il numero massimo di thread, quindi dalla definizione di ThreadPoolExecutor, dovrebbe adattarsi all'attività in un thread o alla coda.
Naturalmente, se non ridimensiono mai il pool, il pool di thread non rifiuta mai alcun invio. Questo è anche difficile da eseguire il debug poiché l'aggiunta di qualsiasi tipo di ritardo negli invii risolve il problema.
Qualche suggerimento su come riparare RejectedExecutionException?
ThreadPoolExecutor
è molto probabilmente una cattiva idea, e in questo caso non avresti bisogno di cambiare anche il codice esistente? Sarebbe meglio che tu fornissi alcuni esempi di come il tuo codice effettivo accede all'esecutore. Sarei sorpreso se usasse molti metodi specifici per ThreadPoolExecutor
(cioè non in ExecutorService
).
ExecutorService
avvolgendone una esistente, che reinvia attività che non sono riuscite a inviare a causa del ridimensionamento?