DEV Community

Cover image for Implementando Paralelismo com Virtual Threads no Java 21
Mattheus Cassundé
Mattheus Cassundé

Posted on

Implementando Paralelismo com Virtual Threads no Java 21

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";
}
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?

Top comments (0)