Neste exemplo, veremos como implementar o paralelismo usando Java 21. Anteriormente, para alcançar paralelismo, utilizávamos e ainda usamos o CompletableFeature
, que funciona muito bem, mas ainda é um pouco verboso.
Para começar, criaremos dois métodos que retornam uma String. Dentro de cada método, faremos a Thread esperar por alguns segundos para simbolizar uma operação de I/O.
private static String executeTask1() throws InterruptedException {
logger.info("task 1");
Thread.sleep(1000);
return "task1";
}
O primeiro método espera 1 segundo antes de retornar a String, e o segundo método é semelhante, também aguardando 1 segundo.
private static String executeTask2() throws InterruptedException {
logger.info("task 2");
Thread.sleep(1000);
return "task2";
}
Agora, criaremos um método que chamará esses dois métodos de forma paralela.
private static String startVirtualThreads() throws InterruptedException, ExecutionException {
try(var executor = Executors.newVirtualThreadPerTaskExecutor()){
var task1 = executor.submit(AppThreadPerTask::executeTask1);
var task2 = executor.submit(AppThreadPerTask::executeTask2);
return task1.get() + task2.get();
}
}
Ao analisar o código acima, observaremos a criação do executor
usando o utilitário Executors
. No Java 21, foi adicionado o método que cria um ExecutorService
usando VirtualThreads
: o newVirtualThreadPerTaskExecutor()
. Com esse método, podemos criar uma Virtual Thread para cada tarefa. Observamos a separação das tarefas nas linhas 4 e 5 do exemplo.
O método submit
recebe Callable<V>
, que é uma interface funcional do Java, e retorna um Future
. O Future
entrega o retorno do método quando este é finalizado com sucesso, funcionando de forma similar às Promises
do JavaScript link.
É importante observar que utilizamos o método .get()
para recuperar os valores dos métodos que estão sendo processados de forma paralela. Se algum dos métodos retornar uma exceção, podemos capturá-la facilmente no método principal.
Agora podemos chamar o método principal, que fará tudo funcionar. É recomendável monitorar os tempos de execução para visualização os tempos.
public static void main(String[] args) throws InterruptedException, ExecutionException {
Instant start = Instant.now();
logger.info("init");
String tasksConcatenated = startVirtualThreads();
logger.info(tasksConcatenated);
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
logger.info("Time taken: "+ timeElapsed.toMillis() +" milliseconds");
}
Após a execução, teremos um log mais ou menos como o seguinte:
2023-10-29 23:03:12 INFO AppThreadPerTask:26 - trace=121212 - init
2023-10-29 23:03:12 INFO AppThreadPerTask:64 - trace= - task 1
2023-10-29 23:03:12 INFO AppThreadPerTask:58 - trace= - task 2
2023-10-29 23:03:17 INFO AppThreadPerTask:29 - trace=121212 - task1task2
2023-10-29 23:03:17 INFO AppThreadPerTask:33 - trace=121212 - Time taken: 5018 milliseconds
Observamos nos logs que os métodos são registrados imediatamente, ou seja, estão sendo executados em paralelo. Isso significa que o método mais lento determinará o tempo de execução do método principal.
Repositório com código de exemplo: Repositório
Você já utilizou alguma dessas funcionalidades em produção? Se sim, o que achou?
Curtiu ? Me segue nas redes 😉