O ThreadPoolExecutor do Java gerencia tarefas assíncronas através de um conjunto de threads reutilizáveis. Quando todos os núcleos estão ocupados e a fila de espera atinge sua capacidade, novas tarefas são rejeitadas. Para tratar essas rejeições, o executor utiliza a interface RejectedExecutionHandler.
Interface RejectedExecutionHandler
A interface possui um único método:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable tarefa, ThreadPoolExecutor executor);
}
Esse método é invocado quando o executor não consegue aceitar uma nova tarefa. Caso nenhum handler seja informado no construtor, o ThreadPoolExecutor adota o AbortPolicy por padrão.
Políticas nativas do JDK
O JDK oferece quatro implementações prontas:
- AbortPolicy: lança
RejectedExecutionException, delegando o tratamento ao chamador. - CallerRunsPolicy: executa a tarefa rejeitada na própria thread que invocou
execute. - DiscardOldestPolicy: descarta a tarefa mais antiga da fila e tenta reenviar a nova.
- DiscardPolicy: descarta a tarefa sem notificar o chamador.
Cenário prático: AbortPolicy
Em um sistema de notificações por push após pagamento, o seguinte padrão foi aplicado:
ThreadPoolExecutor workerPool = new ThreadPoolExecutor(...);
try {
workerPool.execute(() -> notificationService.enviarPush(pedido));
} catch (RejectedExecutionException ex) {
logger.warn("Push rejeitado: {}", pedido.getId(), ex);
}
Como o AbortPolicy é o padrão, a exceção precisa ser capturada para registro ou outra ação compensatória.
Cenário prático: handler personalizado
Para atender a uma regra de retry — duas tentativas com intervalo de 2 segundos entre elas —, uma opção é delegar a lógica de compensação para um handler customizado que persiste a tarefa em uma fila externa, como o Redis:
ThreadPoolExecutor workerPool = new ThreadPoolExecutor(
...,
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable tarefa, ThreadPoolExecutor executor) {
logger.warn("Tarefa rejeitada, enviando para reprocessamento");
redisTemplate.opsForList().leftPush("fila:push", Serializador.toJson(tarefa));
}
}
);
Em seguida, um agendador recupera os itens da fila e os reenvia ao executor. Essa abordagem separa a lógica de execução da lógica de recuperação, respeitando o padrão Strategy.
Cenário prático: CallerRunsPolicy
Em uma plataforma de entrega de payloads HTTP, o processo segue um modelo produtor-consumidor: uma thread periódica busca tarefas pendentes no banco e as entrega ao pool. Quando as chamadas downstream são lentas, o pool fica saturado. Configurar o CallerRunsPolicy faz com que a própria thread de polling execute a tarefa recusada, reduzindo a pressão sobre o banco de dados e evitando que novas buscas sejam realizadas enquanto o sistema está sobrecarregado.
ThreadPoolExecutor dispatcher = new ThreadPoolExecutor(
4,
8,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(128),
new ThreadPoolExecutor.CallerRunsPolicy()
);
Com essa configuração, a thread que coleta as tarefas assume temporariamente o trabalho, agindo como uma válvula de pressão natural para o consumidor.