A classe a seguir envolve um ThreadPoolExecutor e usa um Semaphore para bloquear a fila de trabalho está cheia:
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);
}
}
Essa classe de wrapper é baseada em uma solução fornecida no livro Java Concurrency in Practice de Brian Goetz. A solução no livro leva apenas dois parâmetros do construtor: um Executor
e um limite usado para o semáforo. Isso é mostrado na resposta dada por Fixpoint. Há um problema com essa abordagem: ela pode chegar a um estado em que os threads do pool estão ocupados, a fila está cheia, mas o semáforo acaba de liberar uma licença. ( semaphore.release()
no bloco finalmente). Nesse estado, uma nova tarefa pode obter a licença recém-liberada, mas é rejeitada porque a fila de tarefas está cheia. Claro que isso não é algo que você deseja; você deseja bloquear neste caso.
Para resolver isso, devemos usar uma fila ilimitada , como o JCiP claramente menciona. O semáforo atua como um guarda, dando o efeito de um tamanho de fila virtual. Isso tem o efeito colateral de que é possível que a unidade possa conter maxPoolSize + virtualQueueSize + maxPoolSize
tarefas. Por que é que? Por causa do
semaphore.release()
bloco no último. Se todos os threads do pool chamarem essa instrução ao mesmo tempo, as maxPoolSize
permissões serão liberadas, permitindo que o mesmo número de tarefas entre na unidade. Se estivéssemos usando uma fila limitada, ela ainda estaria cheia, resultando em uma tarefa rejeitada. Agora, como sabemos que isso ocorre apenas quando um thread de pool está quase pronto, isso não é um problema. Sabemos que o encadeamento do pool não será bloqueado, portanto, uma tarefa logo será retirada da fila.
No entanto, você pode usar uma fila limitada. Apenas certifique-se de que seu tamanho seja igual virtualQueueSize + maxPoolSize
. Tamanhos maiores são inúteis, o semáforo impedirá a entrada de mais itens. Tamanhos menores resultarão em tarefas rejeitadas. A chance de as tarefas serem rejeitadas aumenta conforme o tamanho diminui. Por exemplo, digamos que você queira um executor limitado com maxPoolSize = 2 e virtualQueueSize = 5. Em seguida, pegue um semáforo com 5 + 2 = 7 licenças e um tamanho de fila real de 5 + 2 = 7. O número real de tarefas que podem estar na unidade é 2 + 5 + 2 = 9. Quando o executor está cheio (5 tarefas na fila, 2 no pool de threads, portanto, 0 licenças disponíveis) e TODAS as threads do pool liberam suas permissões, então exatamente 2 licenças podem ser obtidas pelas tarefas que chegam.
Agora, a solução do JCiP é um tanto complicada de usar, pois não impõe todas essas restrições (fila ilimitada ou limitada por essas restrições matemáticas, etc.). Acho que isso serve apenas como um bom exemplo para demonstrar como você pode construir novas classes thread-safe com base nas partes que já estão disponíveis, mas não como uma classe totalmente desenvolvida e reutilizável. Não creio que seja esta a intenção do autor.