Java Executor Framework
Envie tarefas a thread pools com Executor e ExecutorService — hierarquia de tipos, factories e regras de dimensionamento.
O capítulo anterior descreveu o que é um thread pool. Este capítulo trata da hierarquia de tipos que você usa para falar com um — as interfaces Executor, ExecutorService e ScheduledExecutorService. Juntas, elas formam o chamado executor framework, introduzido no Java 5 para desacoplar "o trabalho" das "threads que o executam." Você escreve Callable<Result> e Runnable; você envia; o framework cuida da alocação de threads, do enfileiramento e da entrega do resultado.
A hierarquia em três níveis
Executor // execute(Runnable)
|
ExecutorService // + submit/invokeAll/invokeAny/shutdown/awaitTermination
|
ScheduledExecutorService // + schedule/scheduleAtFixedRate/scheduleWithFixedDelayVocê programa na interface mais geral que atende às suas necessidades:
Executor— a base com um único método. Use quando precisar apenas de "dispara e esquece". Um parâmetro de método tipado comoExecutoré o contrato mais genérico "dê-me qualquer coisa que saiba executar umRunnable".ExecutorService— o componente principal. Quase todo o código de produção usa este tipo. Adicionasubmit(com resultado emFuture), operações em lote e ciclo de vida.ScheduledExecutorService— quando você precisa de execução com atraso ou repetição.
Executor.execute — dispara e esquece
public interface Executor {
void execute(Runnable command);
}Essa é a interface completa. execute recebe um Runnable, executa-o em algum momento futuro e não retorna nada. Se o trabalho lançar uma exceção, você não fica sabendo — ela vai para o handler de exceção não capturada da thread trabalhadora.
execute é a chamada correta quando:
- O trabalho não tem valor de retorno.
- Você não precisa aguardá-lo nem obter seu resultado.
- Você não precisa cancelá-lo.
Para qualquer coisa mais rica, use submit.
ExecutorService.submit, a versão completa
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
// ... lifecycle, bulk ops
}submit retorna um Future, que permite:
- Aguardar a conclusão (
get()bloqueia). - Ler o resultado (
get()retorna o valor doCallable). - Cancelar a tarefa (
cancel(boolean mayInterrupt)). - Capturar a exceção da tarefa (
get()a relança).
Cobrimos Future e Callable em detalhes no próximo capítulo; por ora, o contraste com execute é o ponto central. execute é unidirecional; submit abre um canal de retorno.
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> result = pool.submit(() -> {
// Callable<Integer>; can throw, returns a value
return expensive();
});
Integer value = result.get(); // waits, throws ExecutionException if task failedOperações em lote: invokeAll e invokeAny
Quando você tem uma coleção de tarefas:
List<Callable<Integer>> tasks = makeTasks();
List<Future<Integer>> futures = pool.invokeAll(tasks); // run all, wait for all
Integer first = pool.invokeAny(tasks); // run all, return first success, cancel the restinvokeAll(tasks, timeout, unit) as executa, mas desiste após um prazo; tarefas que não terminaram são retornadas como Futures cujo isDone() é verdadeiro, mas foram canceladas.
invokeAny é a ferramenta certa para consultas redundantes — chame três servidores DNS, use o que responder primeiro e cancele os demais.
ScheduledExecutorService — atrasos e repetições
Quando você precisa de um atraso ou de um agendamento periódico:
ScheduledExecutorService sched = Executors.newScheduledThreadPool(2);
sched.schedule(() -> log("once, after 5 seconds"), 5, TimeUnit.SECONDS);
sched.scheduleAtFixedRate(this::flush, 0, 1, TimeUnit.SECONDS);
// runs at t=0, t=1, t=2, ... — even if a run takes longer, the next one queues
sched.scheduleWithFixedDelay(this::poll, 0, 1, TimeUnit.SECONDS);
// runs at t=0, then 1 second AFTER the previous finished — back-to-back delay is what's fixedA diferença entre atFixedRate e withFixedDelay está em se o período é medido entre inícios ou entre o fim de uma execução e o início da próxima. Para "quero fazer flush a cada segundo no relógio", use atFixedRate; para "quero um intervalo de 1 segundo entre execuções independentemente de quanto tempo levem", use withFixedDelay.
Se uma tarefa agendada lançar uma exceção, as execuções futuras são silenciosamente canceladas. O agendador não registra nada. Sempre envolva tarefas agendadas em um try/catch de nível superior para que continuem executando:
sched.scheduleAtFixedRate(() -> {
try { flush(); }
catch (Throwable t) { log.error("flush failed", t); }
}, 0, 1, TimeUnit.SECONDS);Esquecer isso é o bug de agendador mais comum em aplicações Java em produção.
Dimensionando o pool
O tamanho correto do pool depende do que as tarefas fazem.
Para trabalho vinculado à CPU, a regra geral é N + 1 threads em uma máquina com N núcleos. Cada thread mantém um núcleo ocupado; o +1 cobre o raro momento em que uma thread sofre um travamento de memória.
Para trabalho vinculado a I/O, o número correto é muito maior. A fórmula aproximada:
threads = cores * (1 + (wait_time / compute_time))Se suas tarefas ficam 90% do tempo aguardando o banco de dados, o multiplicador é 10x — 80 threads em 8 núcleos. O número exato depende do padrão de I/O específico; profile e ajuste.
Na prática, utilize dois pools: um pequeno para trabalho de CPU e um grande para I/O. Não os misture — uma chamada lenta ao banco de dados dentro de uma thread do pool de CPU bloqueia um núcleo que deveria estar computando.
As virtual threads do Java 21 mudam essa matemática fundamentalmente: bloquear em I/O não desperdiça mais uma platform thread, então você pode usar um executor de virtual-thread-por-tarefa e parar de dimensionar completamente. Cobrimos isso ao final desta parte.
Factories do Executors — referência rápida
Os métodos factory retornam ExecutorService (ou uma sub-interface). Cada um é um ThreadPoolExecutor com valores específicos de configuração:
| Factory | Configuração subjacente | Quando usar |
|---|---|---|
newFixedThreadPool(n) | core=max=n, LinkedBlockingQueue ilimitada | Paralelismo previsível; a fila ilimitada é a armadilha |
newCachedThreadPool | core=0, max=MAX_VALUE, SynchronousQueue, keep-alive de 60s | Tarefas curtas e intermitentes; o número ilimitado de threads é a armadilha |
newSingleThreadExecutor | Igual a newFixedThreadPool(1), mas o pool não é reconfigurável | Serializar um único worker ordenado |
newScheduledThreadPool(n) | n threads base, fila agendada | Tarefas periódicas |
newWorkStealingPool | Java 8+: um ForkJoinPool com paralelismo = núcleos | Trabalho vinculado à CPU, subtarefas recursivas |
newVirtualThreadPerTaskExecutor | Java 21+: uma virtual thread por tarefa | Trabalho vinculado a I/O, servidores web |
Evite newFixedThreadPool e newCachedThreadPool em caminhos de sobrecarga de produção — ambos têm eixos de crescimento ilimitado. Use new ThreadPoolExecutor(...) diretamente com uma fila limitada.
A sequência padrão de encerramento
Um pool que nunca é encerrado mantém suas threads trabalhadoras não-daemon vivas, impedindo a saída da JVM. Todo pool que você criar precisa do mesmo padrão de limpeza:
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
// ... submit work, gather results ...
} finally {
pool.shutdown();
try {
if (!pool.awaitTermination(10, TimeUnit.SECONDS)) {
pool.shutdownNow();
pool.awaitTermination(5, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}Ou, desde o Java 19, o mesmo via try-with-resources:
try (var pool = Executors.newFixedThreadPool(4)) {
pool.submit(...);
pool.submit(...);
} // close() runs shutdown + awaitTerminationO ExecutorService.close() do Java 19 faz o encerramento educado e aguarda indefinidamente; combine-o com um watchdog se você não puder tolerar uma espera infinita.
Um exemplo prático: o framework de ponta a ponta
O programa abaixo usa cada uma das três interfaces — Executor para dispara-e-esquece, ExecutorService para resultados e ScheduledExecutorService para periódicas — tudo em um só lugar.
O que observar na execução:
- A seção 2 usou
try (ExecutorService pool = ...)— o padrão de fechamento no escopo do Java 19. Oclose()do pool executashutdown()e depois aguarda. Essa é a forma mais limpa de encerramento; para código mais antigo ou prazos mais rígidos, volte à sequênciashutdown+awaitTermination+shutdownNow. - A seção 3 executou três tarefas de 50/80/20 ms em 4 workers.
invokeAllretornou apenas após a mais lenta concluir — cerca de 80 ms. Esse é o contrato de "aguardar por todas". Osumsobre os futures foi a soma dos valores retornados, na ordem de submissão. - A seção 4 executou a mesma estrutura com
invokeAny. A tarefa mais rápida (50 ms) retornou primeiro; as demais foram canceladas.invokeAnyé exatamente o formato certo para padrões de "primeira resposta bem-sucedida" — buscas DNS contra múltiplos servidores, downloads de espelhos, corridas de latência. - A seção 5 usou
scheduleAtFixedRatecom período de 60 ms. Cada tick disparou em uma thread do pool agendado. O wrappertry/catchdentro do corpo é o formato de produção — se uma tarefa agendada lançar uma exceção, o agendador silenciosamente cancela execuções futuras. Envolver cada corpo em um catch de nível superior evita que isso aconteça. - A tarefa agendada foi explicitamente cancelada com
cancel(false)antes de o programa sair. Cancelar e encerrar o agendador é o que permite à JVM terminar; sem isso, o agendador mantém threads não-daemon e o programa trava. O mesmo se aplica a todo executor que você criar.
O que vem a seguir
O próximo capítulo, Java Callable and Future, mergulha no lado de tratamento de resultados do submit — Callable<V>, Future<V>, cancelamento e os idiomas padrão para obter um valor de uma tarefa assíncrona.