Implementando Paralelismo com Virtual Threads no Java 21

cassunde

Mattheus Cassundé

Posted on October 30, 2023

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?

💖 💪 🙅 🚩
cassunde
Mattheus Cassundé

Posted on October 30, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related