Como posso contornar essa limitação de ThreadPoolExecutoronde a fila precisa ser limitada e cheia antes que mais threads sejam iniciados.
Acredito que finalmente encontrei uma solução um tanto elegante (talvez um pouco hackeada) para essa limitação com ThreadPoolExecutor. Trata-se LinkedBlockingQueuede estender para que ele retorne falsepara queue.offer(...)quando já houver algumas tarefas na fila. Se os threads atuais não estiverem acompanhando as tarefas enfileiradas, o TPE adicionará threads adicionais. Se o pool já estiver no máximo de threads, o RejectedExecutionHandlerserá chamado. É o manipulador que então faz o put(...)na fila.
Certamente é estranho escrever uma fila onde offer(...)pode retornar falsee put()nunca bloquear, então essa é a parte do hack. Mas isso funciona bem com o uso da fila pelo TPE, então não vejo nenhum problema em fazer isso.
Aqui está o código:
// 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;
}
}
});
Com esse mecanismo, quando eu enviar tarefas para a fila, a ThreadPoolExecutorvontade:
- Dimensione o número de threads até o tamanho do núcleo inicialmente (aqui 1).
- Ofereça para a fila. Se a fila estiver vazia, ela será enfileirada para ser tratada pelos threads existentes.
- Se a fila já tiver 1 ou mais elementos, o
offer(...)retornará falso.
- Se false for retornado, aumente o número de threads no pool até que atinjam o número máximo (aqui 50).
- Se estiver no máximo, ele chama o
RejectedExecutionHandler
- Em
RejectedExecutionHandlerseguida, coloca a tarefa na fila para ser processada pelo primeiro thread disponível na ordem FIFO.
Embora no meu código de exemplo acima, a fila seja ilimitada, você também pode defini-la como uma fila limitada. Por exemplo, se você adicionar uma capacidade de 1000 ao LinkedBlockingQueue, ele irá:
- dimensionar os fios até o máximo
- então enfileire até que esteja cheio com 1000 tarefas
- em seguida, bloqueie o chamador até que haja espaço disponível para a fila.
Além disso, se você precisar usar offer(...)no
RejectedExecutionHandler, poderá usar o offer(E, long, TimeUnit)método em vez de Long.MAX_VALUEcomo o tempo limite.
Aviso:
Se você espera que as tarefas sejam adicionadas ao executor após ele ter sido encerrado, você pode querer ser mais esperto quanto a RejectedExecutionExceptiondescartar nosso serviço personalizado RejectedExecutionHandlerquando o executor-serviço for encerrado. Obrigado a @RaduToader por apontar isso.
Editar:
Outro ajuste para essa resposta poderia ser perguntar ao TPE se há threads inativos e apenas enfileirar o item se houver. Você teria que fazer uma verdadeira classe para isso e adicionar um ourQueue.setThreadPoolExecutor(tpe);método nela.
Então, seu offer(...)método pode ser semelhante a:
- Verifique se
tpe.getPoolSize() == tpe.getMaximumPoolSize()nesse caso basta ligar super.offer(...).
- Caso contrário
tpe.getPoolSize() > tpe.getActiveCount(), chame, super.offer(...)pois parece haver threads inativos.
- Caso contrário, volte
falsepara bifurcar outro tópico.
Talvez isto:
int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
return super.offer(e);
} else {
return false;
}
Observe que os métodos get no TPE são caros, pois acessam volatilecampos ou (no caso de getActiveCount()) bloqueiam o TPE e percorrem a lista de threads. Além disso, existem condições de corrida aqui que podem fazer com que uma tarefa seja enfileirada incorretamente ou outro encadeamento bifurcado quando havia um encadeamento ocioso.