W3docs

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/scheduleWithFixedDelay

Você 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 como Executor é o contrato mais genérico "dê-me qualquer coisa que saiba executar um Runnable".
  • ExecutorService — o componente principal. Quase todo o código de produção usa este tipo. Adiciona submit (com resultado em Future), 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 do Callable).
  • 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 failed

Operaçõ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 rest

invokeAll(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 fixed

A 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:

FactoryConfiguração subjacenteQuando usar
newFixedThreadPool(n)core=max=n, LinkedBlockingQueue ilimitadaParalelismo previsível; a fila ilimitada é a armadilha
newCachedThreadPoolcore=0, max=MAX_VALUE, SynchronousQueue, keep-alive de 60sTarefas curtas e intermitentes; o número ilimitado de threads é a armadilha
newSingleThreadExecutorIgual a newFixedThreadPool(1), mas o pool não é reconfigurávelSerializar um único worker ordenado
newScheduledThreadPool(n)n threads base, fila agendadaTarefas periódicas
newWorkStealingPoolJava 8+: um ForkJoinPool com paralelismo = núcleosTrabalho vinculado à CPU, subtarefas recursivas
newVirtualThreadPerTaskExecutorJava 21+: uma virtual thread por tarefaTrabalho 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 + awaitTermination

O 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.

java— editable, runs on the server

O que observar na execução:

  • A seção 2 usou try (ExecutorService pool = ...) — o padrão de fechamento no escopo do Java 19. O close() do pool executa shutdown() e depois aguarda. Essa é a forma mais limpa de encerramento; para código mais antigo ou prazos mais rígidos, volte à sequência shutdown + awaitTermination + shutdownNow.
  • A seção 3 executou três tarefas de 50/80/20 ms em 4 workers. invokeAll retornou apenas após a mais lenta concluir — cerca de 80 ms. Esse é o contrato de "aguardar por todas". O sum sobre 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 scheduleAtFixedRate com período de 60 ms. Cada tick disparou em uma thread do pool agendado. O wrapper try/catch dentro 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 submitCallable<V>, Future<V>, cancelamento e os idiomas padrão para obter um valor de uma tarefa assíncrona.

Prática

Prática
Você agenda uma tarefa com `scheduleAtFixedRate` e ela lança uma `RuntimeException` na terceira execução. O que acontece com as execuções subsequentes?
Você agenda uma tarefa com `scheduleAtFixedRate` e ela lança uma `RuntimeException` na terceira execução. O que acontece com as execuções subsequentes?
Was this page helpful?