La classe seguente avvolge un ThreadPoolExecutor e utilizza un semaforo per bloccare, quindi la coda di lavoro è piena:
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);
}
}
Questa classe wrapper si basa su una soluzione fornita nel libro Java Concurrency in Practice di Brian Goetz. La soluzione nel libro accetta solo due parametri del costruttore: un Executor
e un limite utilizzato per il semaforo. Questo è mostrato nella risposta data da Fixpoint. C'è un problema con questo approccio: può entrare in uno stato in cui i thread del pool sono occupati, la coda è piena, ma il semaforo ha appena rilasciato un permesso. ( semaphore.release()
nell'ultimo blocco). In questo stato, una nuova attività può acquisire l'autorizzazione appena rilasciata, ma viene rifiutata perché la coda delle attività è piena. Ovviamente questo non è qualcosa che vuoi; si desidera bloccare in questo caso.
Per risolvere questo problema, dobbiamo utilizzare una coda illimitata , come menziona chiaramente JCiP. Il semaforo funge da guardia, dando l'effetto di una dimensione di coda virtuale. Ciò ha l'effetto collaterale che è possibile che l'unità possa contenere maxPoolSize + virtualQueueSize + maxPoolSize
attività. Perché? A causa del
semaphore.release()
blocco finale. Se tutti i thread del pool chiamano questa istruzione contemporaneamente, maxPoolSize
vengono rilasciati i permessi, consentendo allo stesso numero di attività di entrare nell'unità. Se stessimo utilizzando una coda delimitata, sarebbe ancora piena, risultando in un'attività rifiutata. Ora, poiché sappiamo che ciò si verifica solo quando un thread del pool è quasi finito, questo non è un problema. Sappiamo che il thread del pool non si bloccherà, quindi un'attività verrà presto prelevata dalla coda.
Tuttavia, puoi utilizzare una coda limitata. Assicurati solo che le sue dimensioni siano uguali virtualQueueSize + maxPoolSize
. Dimensioni maggiori sono inutili, il semaforo impedirà di far entrare più elementi. Dimensioni più piccole comporteranno attività rifiutate. La possibilità che le attività vengano rifiutate aumenta al diminuire delle dimensioni. Ad esempio, supponi di volere un esecutore delimitato con maxPoolSize = 2 e virtualQueueSize = 5. Quindi prendi un semaforo con 5 + 2 = 7 permessi e una dimensione della coda effettiva di 5 + 2 = 7. Il numero reale di attività che possono essere presenti nell'unità è quindi 2 + 5 + 2 = 9. Quando l'esecutore è pieno (5 attività in coda, 2 nel pool di thread, quindi 0 permessi disponibili) e TUTTI i thread del pool rilasciano i loro permessi, allora esattamente 2 permessi possono essere presi dalle attività in arrivo.
Ora la soluzione di JCiP è un po 'complicata da usare in quanto non applica tutti questi vincoli (coda illimitata o limitata da quelle restrizioni matematiche, ecc.). Penso che questo serva solo come un buon esempio per dimostrare come creare nuove classi thread-safe basate sulle parti già disponibili, ma non come una classe completa e riutilizzabile. Non credo che quest'ultima fosse l'intenzione dell'autore.