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 😉