Coordenação da Execução de Tarefas Assíncronas em Java
Em aplicações multithread, é comum precisar executar várias tarefas em paralelo — seja para obter o resultado da primeira tarefa que terminar com sucesso, seja para combinar os resultados de todas as tarefas concluídas.
A interface ExecutorService oferece dois métodos para esse tipo de coordenação: invokeAll() e invokeAny().
Método invokeAll(): aguardando todas as tarefas
O método invokeAll() envia uma coleção de tarefas do tipo Callable para execução e bloqueia o código que faz a chamada até que todas as tarefas terminem.
Ele retorna uma lista de objetos Future, que contêm o resultado ou o estado de cada tarefa, preservando a mesma ordem da lista de entrada.
Esse método possui duas versões:
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)A primeira executa todas as tarefas sem limite de tempo. A segunda adiciona um tempo máximo de espera (timeout), após o qual as tarefas que ainda não terminaram são interrompidas.
Um possível inconveniente é que o método aguarda a finalização de todas as tarefas, mesmo que algumas falhem com exceção.
Exemplo com invokeAll()
O exemplo a seguir calcula fatoriais de vários números usando tarefas Callable executadas em threads virtuais:
import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
record FactorialResult(int number, int factorial) {}
class FactorialTask implements Callable<FactorialResult> {
private int number;
FactorialTask(int number) { this.number = number; }
public FactorialResult call() throws Exception {
if (number < 1) throw new Exception("Incorrect number: " + number + ". Number must be greater than 0");
int result = 1;
int n = number;
while (n > 0) result *= n--;
return new FactorialResult(number, result);
}
}
class Program {
public static void main(String[] args) {
System.out.println("Main thread started...");
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Callable<FactorialResult>> tasks = new ArrayList<>();
for (int i = -1; i < 4; i++) tasks.add(new FactorialTask(i));
List<Future<FactorialResult>> results = executor.invokeAll(tasks);
for (Future<FactorialResult> result : results) {
if (result.state() == Future.State.SUCCESS) {
FactorialResult factorialResult = result.resultNow();
System.out.printf("Factorial of %d: %d\n",
factorialResult.number(), factorialResult.factorial());
} else {
var ex = result.exceptionNow();
System.out.println("Factorial exception: " + ex.getMessage());
}
}
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
System.out.println("Main thread finished...");
}
}Neste exemplo:
FactorialResulté um record que armazena o número e o resultado do cálculo do fatorial.FactorialTaskimplementaCallable<FactorialResult>e calcula o fatorial do número recebido no construtor.- O
ExecutorServiceé criado comExecutors.newVirtualThreadPerTaskExecutor(), que executa cada tarefa em uma thread virtual.
O método invokeAll() envia todas as tarefas e bloqueia até que todas terminem.
Depois, percorre a lista de Future retornados:
- Se o estado da tarefa for
SUCCESS, o métodoresultNow()retorna o resultado. - Se a tarefa falhou,
exceptionNow()retorna a exceção correspondente.
Saída esperada:
Main thread started... Factorial exception: Incorrect number: -1. Number must be greater than 0 Factorial exception: Incorrect number: 0. Number must be greater than 0 Factorial of 1: 1 Factorial of 2: 2 Factorial of 3: 6 Main thread finished...
Método invokeAny(): aguardando apenas uma tarefa
O método invokeAny() também recebe uma coleção de tarefas Callable, mas retorna o resultado da primeira tarefa que terminar com sucesso.
As demais tarefas são canceladas automaticamente.
Suas duas versões são:
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)A primeira aguarda indefinidamente até uma tarefa ser concluída com sucesso.
A segunda define um tempo máximo de espera.
Se nenhuma tarefa terminar com sucesso dentro do tempo, ou se todas falharem, é lançada uma exceção ExecutionException.
Exemplo com invokeAny()
import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
record FactorialResult(int number, int factorial) {}
class FactorialTask implements Callable<FactorialResult> {
private int number;
FactorialTask(int number) { this.number = number; }
public FactorialResult call() throws Exception {
if (number < 1) throw new Exception("Incorrect number: " + number + ". Number must be greater than 0");
int result = 1;
int n = number;
while (n > 0) result *= n--;
return new FactorialResult(number, result);
}
}
class Program {
public static void main(String[] args) {
System.out.println("Main thread started...");
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Callable<FactorialResult>> tasks = new ArrayList<>();
for (int i = -1; i < 4; i++) tasks.add(new FactorialTask(i));
FactorialResult factorialResult = executor.invokeAny(tasks);
System.out.printf("Factorial of %d: %d\n",
factorialResult.number(), factorialResult.factorial());
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
System.out.println("Main thread finished...");
}
}Nesse caso, todas as tarefas são iniciadas simultaneamente. O método invokeAny() retorna o resultado da primeira tarefa concluída com sucesso, cancelando as demais.
Possível saída:
Main thread started... Factorial of 3: 6 Main thread finished...
O resultado depende da ordem de término das tarefas, portanto pode variar a cada execução.
Se todas as tarefas falharem (por exemplo, se os números forem inválidos), uma exceção é lançada e capturada no bloco catch:
Main thread started... java.lang.Exception: Incorrect number: -1. Number must be greater than 0 Main thread finished...
Resumo
invokeAll()executa todas as tarefas e retorna uma lista deFutureapós a conclusão de todas.invokeAny()retorna o resultado da primeira tarefa concluída com sucesso e cancela as demais.- Ambas as funções bloqueiam a thread que faz a chamada até o término das tarefas.
- Em
invokeAll(), cadaFuturepode ser consultado para obter o resultado (resultNow()) ou exceção (exceptionNow()). - O
ExecutorServicepode ser criado com diferentes métodos da classeExecutors, incluindo o uso de threads virtuais.