Java ExecutorService
Na aula de Threads, você criou cada thread à mão com new Thread. Em programas de verdade quase ninguém faz isso: usa-se um ExecutorService ("serviço executor"), um time de threads prontas para trabalhar. Você entrega tarefas ao time, e ele cuida de quem faz o quê. De quebra, as tarefas podem devolver resultados.
- O que é um pool de threads e como criar um com
Executors.newFixedThreadPool. - Enviar tarefas com
submite buscar o resultado comFuture.get. - Enviar uma lista inteira com
invokeAll. - Encerrar o time:
shutdown,awaitTerminatione o executor emtry-with-resources Java 19+; e o que acontece se você esquecer. - O básico de
CompletableFuture:supplyAsync,thenApplyejoin.
Esta aula continua a de Threads e usa lambdas, generics e try-with-resources. Os exemplos rodaram no JDK 25 da máquina da Ana, na pasta ~/java.
A ideia antes do código
Pense numa central de entregas. A Ana não contrata um motoboy novo para cada pedido e o demite na volta: isso seria caro e lento. Ela tem uma equipe fixa de, digamos, dois motoboys, e um quadro de pedidos. Cada pedido novo vai para o quadro; quem estiver livre pega o próximo. No fim do dia, ela avisa: "não entram mais pedidos; terminem o que falta e podem ir".
- A equipe fixa é o pool de threads (pool = "grupo", "reserva").
- O quadro é a fila de tarefas.
- O pedido é a tarefa: um
Runnable(não devolve nada) ou umCallable(devolve um resultado). - O comprovante que a Ana recebe ao entregar um pedido ao quadro é o
Future("futuro"): com ele, ela pergunta depois "já ficou pronto? qual o resultado?". - "Não entram mais pedidos" é o
shutdown().
1. Criar o time e enviar tarefas
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) throws InterruptedException {
ExecutorService equipe = Executors.newFixedThreadPool(2);
for (int i = 1; i <= 5; i++) {
int pedido = i;
equipe.submit(() -> {
System.out.println(Thread.currentThread().getName() + " embalou o pedido " + pedido);
});
}
equipe.shutdown();
equipe.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("Expediente encerrado.");
}
}
$ javac Main.java && java Main pool-1-thread-1 embalou o pedido 1 pool-1-thread-2 embalou o pedido 2 pool-1-thread-1 embalou o pedido 3 pool-1-thread-2 embalou o pedido 4 pool-1-thread-1 embalou o pedido 5 Expediente encerrado.
| Linha | O que faz |
|---|---|
import java.util.concurrent.… | O pacote da concorrência (concurrent = "simultâneo"). |
Executors.newFixedThreadPool(2) | Cria um time fixo de 2 threads. Executors (com "s") é a "fábrica" de executores. |
int pedido = i; | Uma cópia de i para a lambda. Lambdas só podem usar variáveis que não mudam depois (effectively final), e i muda a cada volta. |
equipe.submit(() -> { … }) | "Submeter": põe a tarefa na fila. Volta na hora, sem esperar a tarefa rodar. |
equipe.shutdown() | "Desligar": não aceita mais tarefas, mas termina as que estão na fila. |
awaitTermination(10, TimeUnit.SECONDS) | "Aguardar o término": a main espera o time terminar tudo, por no máximo 10 segundos. |
Cinco pedidos e só duas threads: pool-1-thread-1 e pool-1-thread-2 (os nomes que o Java dá às threads do primeiro pool) se revezaram. As threads foram reaproveitadas: nenhuma foi criada para um pedido só. Como na aula anterior, a ordem e a divisão entre as threads podem mudar de uma execução para outra.
2. Tarefas que devolvem resultado: submit + Future.get
Se a lambda devolve um valor, ela é um Callable ("chamável"), e o submit entrega um Future com o resultado prometido. A Ana precisa consultar o estoque de três frutas num sistema lento, que demora 1 segundo por consulta:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
static int consultarEstoque(String fruta) throws InterruptedException {
Thread.sleep(1000); // finge que demora: consulta a um sistema lento
return fruta.length() * 10;
}
public static void main(String[] args) throws Exception {
ExecutorService equipe = Executors.newFixedThreadPool(3);
long inicio = System.currentTimeMillis();
Future<Integer> maca = equipe.submit(() -> consultarEstoque("maçã"));
Future<Integer> banana = equipe.submit(() -> consultarEstoque("banana"));
Future<Integer> uva = equipe.submit(() -> consultarEstoque("uva"));
System.out.println("Pedidos enviados; esperando as respostas...");
int total = maca.get() + banana.get() + uva.get();
long ms = System.currentTimeMillis() - inicio;
System.out.println("Estoque total: " + total);
System.out.println("Tempo: cerca de " + ms / 100 * 100 + " ms");
equipe.shutdown();
}
}
$ javac Main.java && java Main Pedidos enviados; esperando as respostas... Estoque total: 130 Tempo: cerca de 1000 ms
- Future<Integer>
- "Um
Integerque vai existir no futuro." É o comprovante do pedido. - maca
- O nome da variável.
- = equipe.submit(
- Entrega a tarefa ao time e recebe o comprovante na hora.
- () -> consultarEstoque("maçã"))
- A tarefa: um
Callable<Integer>, porque devolve umint. UmCallablepode lançar exceções checked (oInterruptedExceptiondosleep), diferente doRunnable.
E maca.get() ("pegar") espera o resultado ficar pronto e o devolve. As três consultas rodaram ao mesmo tempo, então o total levou cerca de 1 segundo, e não 3. Para conferir, a Ana trocou newFixedThreadPool(3) por newFixedThreadPool(1) (um motoboy só):
$ javac Main.java && java Main Pedidos enviados; esperando as respostas... Estoque total: 130 Tempo: cerca de 3000 ms
Com uma thread, as consultas fizeram fila: 3 segundos. Esse é o ganho de um pool quando as tarefas passam a maior parte do tempo esperando (rede, disco, outro sistema).
Missão: envie ao time as duas contas como tarefas com submit (uma devolve 12 + 8, a outra 7 + 3), guarde os dois Future<Integer> e mostre Total: 30 somando os resultados com get().
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
ExecutorService equipe = Executors.newFixedThreadPool(2);
// envie as duas tarefas e some os resultados
equipe.shutdown();
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
ExecutorService equipe = Executors.newFixedThreadPool(2);
Future<Integer> manha = equipe.submit(() -> 12 + 8);
Future<Integer> tarde = equipe.submit(() -> 7 + 3);
System.out.println("Total: " + (manha.get() + tarde.get()));
equipe.shutdown();
}
}
3. Uma lista de tarefas: invokeAll e o executor em try
Quando as tarefas já estão numa lista, invokeAll ("invocar todas") envia todas, espera todas terminarem e devolve a lista de Futures na mesma ordem. E desde o Java 19, o ExecutorService pode ficar num try-with-resources: ao sair do bloco, ele faz o shutdown e espera as tarefas sozinho.
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
List<String> frutas = List.of("maçã", "banana", "uva", "kiwi");
List<Callable<String>> tarefas = frutas.stream()
.map(f -> (Callable<String>) () -> f.toUpperCase() + " (" + Thread.currentThread().getName() + ")")
.toList();
try (ExecutorService equipe = Executors.newFixedThreadPool(2)) {
List<Future<String>> resultados = equipe.invokeAll(tarefas);
for (Future<String> r : resultados) {
System.out.println(r.get());
}
}
System.out.println("Fim");
}
}
$ javac Main.java && java Main MAÇÃ (pool-1-thread-1) BANANA (pool-1-thread-2) UVA (pool-1-thread-2) KIWI (pool-1-thread-2) Fim
Os resultados saíram na ordem da lista de frutas, mesmo que as tarefas tenham terminado em outra ordem: quem garante isso é a lista de Futures. A coluna entre parênteses mostra qual thread fez cada uma (a thread-2 pegou três: estava livre antes). O .map(…) transforma cada fruta numa tarefa (veja streams); o (Callable<String>) diz ao compilador que tipo de lambda é aquela.
| Encerrar o executor | Como | Desde |
|---|---|---|
| À mão | shutdown() + awaitTermination(tempo, unidade) | Java 5 |
| Automático | try (ExecutorService e = …) { … }: o close() faz o shutdown e espera | Java 19+ |
| Urgente | shutdownNow(): tenta interromper as tarefas em andamento e devolve as que nem começaram | Java 5 |
4. CompletableFuture: encadear passos
Com o Future comum, a única forma de usar o resultado é parar e esperar com get(). O CompletableFuture ("futuro completável") permite dizer o que fazer depois, como uma receita: "busque o preço; quando chegar, aplique o desconto; depois monte a etiqueta".
import java.util.concurrent.CompletableFuture;
public class Main {
static double buscarPreco(String produto) {
System.out.println("Buscando " + produto + " em " + Thread.currentThread().getName());
return 50.0;
}
public static void main(String[] args) {
CompletableFuture<String> etiqueta = CompletableFuture
.supplyAsync(() -> buscarPreco("café"))
.thenApply(preco -> preco * 0.9)
.thenApply(preco -> "Café com desconto: R$ " + preco);
System.out.println("main continua livre...");
System.out.println(etiqueta.join());
}
}
$ javac Main.java && java Main main continua livre... Buscando café em ForkJoinPool.commonPool-worker-1 Café com desconto: R$ 45.0
- CompletableFuture.supplyAsync(
- "Fornecer de forma assíncrona": roda a lambda em outra thread e promete o resultado. Sem um executor indicado, usa o pool comum do Java (
ForkJoinPool.commonPool, como mostra a saída). - .thenApply(preco -> preco * 0.9)
- "Depois, aplique": quando o preço chegar, transforma-o (10% de desconto). Devolve outro
CompletableFuture, por isso dá para encadear. - .thenApply(preco -> "…" + preco)
- Mais um passo: transforma o número em texto.
- etiqueta.join()
- "Juntar": espera o resultado final e o devolve, como o
get(), mas sem exceção checked.
Repare que a main mostrou "continua livre" antes mesmo de a busca começar: montar a receita não trava ninguém. Só o join() espera. Outros métodos úteis:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<Double> cafe = CompletableFuture.supplyAsync(() -> 18.0);
CompletableFuture<Double> pao = CompletableFuture.supplyAsync(() -> 6.5);
double total = cafe.thenCombine(pao, Double::sum).join();
System.out.println("Total do pedido: " + total);
CompletableFuture<Integer> erro = CompletableFuture.supplyAsync(() -> 10 / 0);
int r = erro.exceptionally(e -> {
System.out.println("Deu problema: " + e.getMessage());
return -1;
}).join();
System.out.println("Resultado: " + r);
}
}
$ javac Main.java && java Main Total do pedido: 24.5 Deu problema: java.lang.ArithmeticException: / by zero Resultado: -1
thenCombine ("depois, combine") junta dois resultados independentes: café e pão foram buscados ao mesmo tempo e somados com Double::sum (uma referência de método). exceptionally ("em caso de exceção") dá um valor de reserva quando algum passo falha.
Formas diferentes de criar o executor
| Fábrica | O time | Quando usar |
|---|---|---|
Executors.newFixedThreadPool(n) | n threads fixas | O caso geral. Para contas pesadas, n = número de núcleos (Runtime.getRuntime().availableProcessors()). |
Executors.newSingleThreadExecutor() | 1 thread | Tarefas que precisam rodar uma de cada vez, em ordem. |
Executors.newCachedThreadPool() | Cria threads conforme a necessidade e reaproveita as livres | Muitas tarefas curtas (cuidado: sem limite de threads). |
Executors.newVirtualThreadPerTaskExecutor() Java 21+ | Uma thread virtual nova por tarefa | Milhares de tarefas que esperam (rede, disco). Veja Threads Virtuais. |
CompletableFuture.supplyAsync(…) | Pool comum do Java (ou um executor que você passar) | Encadear passos assíncronos. |
O simulador executa as tarefas do ExecutorService em sequência, numa thread só, e o Thread.sleep não espera de verdade. As missões funcionam, mas os tempos medidos e a mistura das saídas só aparecem no JDK real, como nos terminais desta página.
Quando dá errado
Esquecer o shutdown: o programa não termina
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService equipe = Executors.newFixedThreadPool(2);
equipe.submit(() -> System.out.println("Pedido embalado"));
System.out.println("Fim do main");
}
}
$ javac Main.java $ timeout 5 java Main « o timeout encerra o programa à força depois de 5 segundos Fim do main Pedido embalado $ echo $? 124 « 124 = o timeout teve de matar o programa
O que aconteceu: o trabalho terminou, mas as threads do pool continuaram vivas, esperando novos pedidos que nunca vão chegar. A JVM só termina quando as threads comuns acabam, então o programa ficaria parado para sempre (sem o timeout, você teria de apertar Ctrl+C). Conserto: shutdown() no fim, ou o executor num try-with-resources.
A tarefa lançou uma exceção: ExecutionException
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
try (ExecutorService equipe = Executors.newFixedThreadPool(2)) {
Future<Integer> conta = equipe.submit(() -> 100 / 0);
System.out.println("Tarefa enviada");
System.out.println(conta.get());
}
}
}
$ javac Main.java && java Main Tarefa enviada Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:124) at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:193) at Main.main(Main.java:10) Caused by: java.lang.ArithmeticException: / by zero at Main.lambda$main$0(Main.java:8) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614) at java.base/java.lang.Thread.run(Thread.java:1474)
O que quer dizer: a divisão por zero aconteceu dentro da tarefa, numa thread do pool (linha 8, lambda$main$0). O pool guardou a exceção no Future, e ela só apareceu no get() (linha 10), embrulhada numa ExecutionException ("exceção de execução"). A causa verdadeira vem em Caused by:. Conserto: trate a ExecutionException com try/catch em volta do get() e olhe e.getCause(). Atenção: se você nunca chamar get(), a exceção some em silêncio.
Enviar tarefa depois do shutdown
ExecutorService equipe = Executors.newFixedThreadPool(2);
equipe.shutdown();
equipe.submit(() -> System.out.println("Pedido atrasado"));
$ javac Main.java && java Main Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@33909752[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@70dea4e[Wrapped task = Main$$Lambda/0x000000009e040210@5c647e05]] rejected from java.util.concurrent.ThreadPoolExecutor@764c12b6[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0] at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2032) at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:787) at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1328) at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:125) at Main.main(Main.java:8)
O que quer dizer: "execução rejeitada". A mensagem é enorme, mas o essencial está no começo (RejectedExecutionException), no meio (rejected from … ThreadPoolExecutor) e no estado do pool (Terminated, encerrado). Os números depois de @ mudam a cada execução. Conserto: envie todas as tarefas antes do shutdown().
Referência
| Código | O que faz |
|---|---|
Executors.newFixedThreadPool(n) | Cria um time de n threads |
ex.submit(runnable) / ex.submit(callable) | Envia a tarefa; devolve um Future |
ex.execute(runnable) | Envia sem devolver Future |
ex.invokeAll(listaDeCallables) | Envia todas, espera todas; devolve List<Future> |
ex.shutdown() | Não aceita mais tarefas; termina as da fila |
ex.awaitTermination(t, TimeUnit.SECONDS) | Espera o fim, até o tempo limite |
try (ExecutorService ex = …) { } | Fecha e espera sozinho (Java 19+) |
future.get() | Espera e devolve o resultado (ExecutionException se a tarefa falhou) |
future.isDone() | Já terminou? |
CompletableFuture.supplyAsync(() -> …) | Começa um cálculo assíncrono |
cf.thenApply(x -> …) | Transforma o resultado quando chegar |
cf.thenCombine(outro, (a, b) -> …) | Junta dois resultados |
cf.exceptionally(e -> valor) | Valor de reserva se der erro |
cf.join() | Espera e devolve o resultado |
Exercícios
1. Complete para criar um time fixo de 4 threads:
2. Complete para esperar o resultado do Future f:
3. Complete para transformar o resultado quando ele chegar:
4. Um programa cria um pool, envia tarefas e não chama shutdown(). O que acontece?
5. Uma tarefa enviada com submit lança ArithmeticException. Onde você fica sabendo?
Missão: os três caixas da loja são tarefas que devolvem o faturamento de cada um. Abra o executor num try-with-resources, use invokeAll e some os resultados para mostrar Faturamento do dia: 245.
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
List<Callable<Integer>> caixas = List.of(() -> 120, () -> 85, () -> 40);
int soma = 0;
// abra o executor num try, chame invokeAll e some
System.out.println("Faturamento do dia: " + soma);
}
}
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
List<Callable<Integer>> caixas = List.of(() -> 120, () -> 85, () -> 40);
int soma = 0;
try (ExecutorService equipe = Executors.newFixedThreadPool(3)) {
for (Future<Integer> f : equipe.invokeAll(caixas)) {
soma = soma + f.get();
}
}
System.out.println("Faturamento do dia: " + soma);
}
}
Missão final: monte uma receita com CompletableFuture: supplyAsync fornece o preço 50.0, um thenApply aplica 10% de desconto (* 0.9), outro monta o texto "Preço final: " + preco, e o join() busca o resultado para mostrar.
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
String r = ""; // troque pela receita com CompletableFuture
System.out.println(r);
}
}
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
String r = CompletableFuture
.supplyAsync(() -> 50.0)
.thenApply(preco -> preco * 0.9)
.thenApply(preco -> "Preço final: " + preco)
.join();
System.out.println(r);
}
}
Resumo
- Um
ExecutorServiceé um time de threads reaproveitáveis; você envia tarefas, não cria threads. submitdevolve umFuture;get()espera o resultado (e lançaExecutionExceptionse a tarefa falhou).invokeAllenvia e espera uma lista inteira.- Sempre encerre:
shutdown()+awaitTermination, outry-with-resources Java 19+. Sem isso, o programa não termina. CompletableFutureencadeia passos:supplyAsync→thenApply→join.- O ganho de tempo vem quando as tarefas esperam (3 consultas de 1 s: cerca de 1000 ms com 3 threads, 3000 ms com 1).
Próxima aula: Threads Virtuais, para quando são milhares de tarefas esperando.