Come posso aggirare questa limitazione in ThreadPoolExecutor
cui la coda deve essere delimitata e piena prima che vengano avviati più thread.
Credo di aver finalmente trovato una soluzione un po 'elegante (forse un po' complicata) a questa limitazione con ThreadPoolExecutor
. Coinvolge l'estensione LinkedBlockingQueue
di averlo ritornare false
per queue.offer(...)
quando ci sono già alcune operazioni in coda. Se i thread correnti non tengono il passo con le attività in coda, il TPE aggiungerà thread aggiuntivi. Se il pool è già al numero massimo di thread, RejectedExecutionHandler
verrà chiamato. È l'handler che poi entra put(...)
in coda.
È certamente strano scrivere una coda in cui offer(...)
può tornare false
e put()
non si blocca mai, quindi questa è la parte hack. Ma questo funziona bene con l'utilizzo della coda da parte di TPE, quindi non vedo alcun problema nel farlo.
Ecco il codice:
// 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);
// we do this after the put() to stop race conditions
if (executor.isShutdown()) {
throw new RejectedExecutionException(
"Task " + r + " rejected from " + e);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
});
Con questo meccanismo, quando invio le attività alla coda, il ThreadPoolExecutor
:
- Ridimensiona inizialmente il numero di thread fino alla dimensione del core (qui 1).
- Offrilo alla coda. Se la coda è vuota, verrà accodata per essere gestita dai thread esistenti.
- Se la coda ha già 1 o più elementi,
offer(...)
restituirà false.
- Se viene restituito false, aumentare il numero di thread nel pool fino a raggiungere il numero massimo (qui 50).
- Se al massimo, chiama il file
RejectedExecutionHandler
- Le
RejectedExecutionHandler
mette allora il compito nella coda da elaborare dal primo thread disponibili in ordine FIFO.
Sebbene nel mio codice di esempio sopra, la coda sia illimitata, potresti anche definirla come coda limitata. Ad esempio, se aggiungi una capacità di 1000 a, LinkedBlockingQueue
allora:
- scalare i fili fino a max
- quindi mettiti in coda finché non è pieno di 1000 attività
- quindi blocca il chiamante fino a quando lo spazio non diventa disponibile per la coda.
Inoltre, se fosse necessario utilizzare offer(...)
in,
RejectedExecutionHandler
è possibile utilizzare il offer(E, long, TimeUnit)
metodo invece con Long.MAX_VALUE
come timeout.
Avvertimento:
Se ti aspetti che le attività vengano aggiunte all'esecutore dopo che è stato chiuso, allora potresti voler essere più intelligente nel buttare RejectedExecutionException
fuori dalla nostra abitudine RejectedExecutionHandler
quando il servizio dell'esecutore è stato chiuso. Grazie a @RaduToader per averlo segnalato.
Modificare:
Un'altra modifica a questa risposta potrebbe essere quella di chiedere al TPE se ci sono thread inattivi e accodare l'elemento solo se è così. Dovresti creare una vera classe per questo e aggiungere un ourQueue.setThreadPoolExecutor(tpe);
metodo su di essa.
Quindi il tuo offer(...)
metodo potrebbe essere simile a:
- Controlla per vedere se
tpe.getPoolSize() == tpe.getMaximumPoolSize()
nel qual caso chiama super.offer(...)
.
- Altrimenti se
tpe.getPoolSize() > tpe.getActiveCount()
poi chiama super.offer(...)
poiché sembrano esserci thread inattivi.
- Altrimenti torna
false
a forkare un altro thread.
Forse questo:
int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
return super.offer(e);
} else {
return false;
}
Si noti che i metodi get su TPE sono costosi poiché accedono ai volatile
campi o (nel caso di getActiveCount()
) bloccano il TPE e percorrono l'elenco dei thread. Inoltre, sono presenti condizioni di competizione che possono causare l'accodamento errato di un'attività o il fork di un altro thread quando era presente un thread inattivo.