Como otimizar o desempenho do Java com threads virtuais, programação reativa e MongoDB
Avalie esse Artigo
Quando ouvi pela primeira vez sobre Project Loom e threads virtuais, meu primeiro pensamento foi que isso era uma sentença de morte para aprogramação reativa. Não foi uma má notícia no início, porque a programação reativa vem com sua camada adicional de complexidade e usar a programação imperativa sem desperdiçar recursos era música para meus ouvidos.
Mas eu estava realmente errado e um pouco mais de leitura e aprendizado me ajudaram a entender por que pensar que isso era um erro.
Nesta publicação, exploraremos threads virtuais e programação reativa, suas diferenças e como podemos aproveitar ambas no mesmo projeto para obter o desempenho máximo de simultaneidade em Java.
Saiba mais sobre osuporte a threads virtuais com o MongoDB em minha postagem anterior sobre este tópico.
Na simultaneidade Java tradicional, threads são entidades pesadas gerenciadas pelo sistema operacional. Cada thread do sistema operacional é envolvido por um thread da plataforma que é gerenciado pela máquina virtual Java (JVM) que executa o código Java.
Cada thread requer recursos significativos do sistema, o que leva a limitações na escalabilidade ao lidar com um grande número de tarefas simultâneas. A alternância de contexto entre threads também consome muitos recursos e pode deteriorar o desempenho.
Os threads virtuais, introduzidos pelo Projeto Loom no JEP 444, são leves por design e têm como objetivo superar as limitações dos threads tradicionais e criar aplicativos simultâneos de alta taxa de transferência. Eles implementam o
java.lang.Thread
e são gerenciados pela JVM. Vários deles podem ser executados no mesmo thread de plataforma, tornando-os mais eficientes para trabalhar com um grande número de pequenas tarefas simultâneas.Os threads virtuais permitem que o desenvolvedor Java use os recursos do sistema com mais eficiência e E/S sem bloqueio.
Mas com o JEP 453: Simultaneidade Estruturada e JEP 446: Valores com Escopo, os threads virtuais também suportam simultaneidade estruturada para tratar um grupo de tarefas relacionadas como uma única unidade de trabalho e dividir uma tarefa em subtarefas independentes menores para melhorar tempo de resposta e taxa de transferência.
Aqui está um exemplo básico de Java.
1 import java.util.concurrent.ExecutorService; 2 import java.util.concurrent.Executors; 3 4 public class VirtualThreadsExample { 5 6 public static void main(String[] args) { 7 try (ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) { 8 for (int i = 0; i < 10; i++) { 9 int taskNumber = i + 1; 10 Runnable task = () -> taskRunner(taskNumber); 11 virtualExecutor.submit(task); 12 } 13 } 14 } 15 16 private static void taskRunner(int number) { 17 System.out.println("Task " + number + " executed by virtual thread: " + Thread.currentThread()); 18 } 19 }
Saída desse programa:
1 Task 6 executed by virtual thread: VirtualThread[#35]/runnable@ForkJoinPool-1-worker-6 2 Task 2 executed by virtual thread: VirtualThread[#31]/runnable@ForkJoinPool-1-worker-2 3 Task 10 executed by virtual thread: VirtualThread[#39]/runnable@ForkJoinPool-1-worker-10 4 Task 1 executed by virtual thread: VirtualThread[#29]/runnable@ForkJoinPool-1-worker-1 5 Task 5 executed by virtual thread: VirtualThread[#34]/runnable@ForkJoinPool-1-worker-5 6 Task 7 executed by virtual thread: VirtualThread[#36]/runnable@ForkJoinPool-1-worker-7 7 Task 4 executed by virtual thread: VirtualThread[#33]/runnable@ForkJoinPool-1-worker-4 8 Task 3 executed by virtual thread: VirtualThread[#32]/runnable@ForkJoinPool-1-worker-3 9 Task 8 executed by virtual thread: VirtualThread[#37]/runnable@ForkJoinPool-1-worker-8 10 Task 9 executed by virtual thread: VirtualThread[#38]/runnable@ForkJoinPool-1-worker-9
Podemos ver que as tarefas executadas em paralelo — cada uma em um thread virtual diferente, gerenciado por um único
ForkJoinPool
e seus trabalhadores associados.Em primeiro lugar, aprogramação reativa é um paradigma de programação, enquanto os threads virtuais são ", apenas ", uma solução técnica. A programação reativa gira em torno de princípios de programação assíncrona e orientada por eventos, oferecendo soluções para gerenciar fluxosde dados e operações assíncronas com eficiência.
Os colunas da programação reativa são:
- Non-blocking I/O.
- Comunicação assíncrona baseada em fluxo.
- Manuseio de pressão de retorno para evitar componentes downstream sobrecarregados com mais dados do que eles podem gerenciar.
O único ponto de interesse comum com os threads virtuais é o primeiro: E/S sem bloqueio.
As principais estruturas em Java que seguem os princípios de programação reativa são:
- Reactive Streams: fornece um padrão para processamento de fluxo assíncrono com contrapressão sem bloqueio.
- Projeto Reactor: base da pilha reativa no ecossistema da primavera.
O MongoDB também oferece uma implementação da API Reactive Streams: o MongoDB Reactive Streams Driver.
Aqui está um exemplo em que inserir um documento no MongoDB e, em seguida, recuperá-lo.
1 import com.mongodb.client.result.InsertOneResult; 2 import com.mongodb.quickstart.SubscriberHelpers.OperationSubscriber; 3 import com.mongodb.quickstart.SubscriberHelpers.PrintDocumentSubscriber; 4 import com.mongodb.reactivestreams.client.MongoClient; 5 import com.mongodb.reactivestreams.client.MongoClients; 6 import com.mongodb.reactivestreams.client.MongoCollection; 7 import org.bson.Document; 8 9 public class MongoDBReactiveExample { 10 11 public static void main(String[] args) { 12 try (MongoClient mongoClient = MongoClients.create("mongodb://localhost")) { 13 MongoCollection<Document> coll = mongoClient.getDatabase("test").getCollection("testCollection"); 14 15 Document doc = new Document("reactive", "programming"); 16 17 var insertOneSubscriber = new OperationSubscriber<InsertOneResult>(); 18 coll.insertOne(doc).subscribe(insertOneSubscriber); 19 insertOneSubscriber.await(); 20 21 var printDocumentSubscriber = new PrintDocumentSubscriber(); 22 coll.find().first().subscribe(printDocumentSubscriber); 23 printDocumentSubscriber.await(); 24 } 25 } 26 }
Observação: as
SubscriberHelpers.OperationSubscriber
SubscriberHelpers.PrintDocumentSubscriber
classes e vêm do Guia de início rápido do Reactive Streams. Você pode encontrar o SubscriberHelpers.java nos exemplos de códigodo repositório do driver Java do MongoDB .Como você deve ter entendido, threads virtuais e programação reativa não estão competindo entre si, e eles certamente concordam em uma coisa: bloquear operações de E/S é mau!
Quem disse que precisvamos fazer uma escolha? Por que não usar os dois para atingir o desempenho máximo e evitar o bloqueio de E/S de uma vez por todas?
Boas notícias: a biblioteca
reactor-core
adicionou suporte a threads virtuais no 3.6.0. Project Reactor é a biblioteca que fornece uma implementação rica e funcional do Reactive Streams APIs
no Spring Boot e WebFlux.Isso significa que podemos usar threads virtuais em um projeto Spring Boot que esteja usando o MongoDB Reactive Streams Driver e Webflux.
No entanto, existem algumas condições:
- Ativar threads virtuais:
spring.threads.virtual.enabled=true
emapplication.properties
.
No repositório, meu colega Wen Jie Teo e eu atualizamos o
pom.xml
e oapplication.properties
para que pudéssemos usar threads virtuais nesse projeto reativo.Você pode executar os comandos a seguir para executar esse projeto rapidamente e testar se ele está sendo executado corretamente com threads virtuais. Você pode obter mais detalhes no arquivoREADME.md, mas aqui está a essência.
Aqui estão as instruções em inglês:
- Clone o repositório e acesse a pasta.
- Atualize o nível de registro em
application.properties
parainfo
. - Execute o script
setup.js
para inicializar a coleçãoaccounts
. - Inicie o aplicativo Java.
- Teste uma das APIs disponíveis.
Aqui estão as instruções convertidas para o Bash.
Primeiro terminal:
1 git clone git@github.com:mongodb-developer/mdb-spring-boot-reactive.git 2 cd mdb-spring-boot-reactive/ 3 sed -i 's/warn/info/g' src/main/resources/application.properties 4 docker run --rm -d -p 27017:27017 -h $(hostname) --name mongo mongo:latest --replSet=RS && sleep 5 && docker exec mongo mongosh --quiet --eval "rs.initiate();" 5 mongosh --file setup.js 6 mvn spring-boot:run
Observação: no macOS, talvez seja necessário usar
sed -i '' 's/warn/info/g' src/main/resources/application.properties
se não estiver usando gnu-sed
, ou você pode simplesmente editar o arquivo manualmente.Segundo terminal
1 curl 'localhost:8080/account' -H 'Content-Type: application/json' -d '{"accountNum": "1"}'
Se tudo funcionar conforme o planejado, você deverá ver esta linha no primeiro terminal (onde está executando o Spring).
1 Stack trace's last line: java.base/java.lang.VirtualThread.run(VirtualThread.java:309) from POST /account
Esta é a última linha no traçado de pilha que estamos registrando. Isso prova que estamos usando threads virtuais para lidar com nossa query.
Se desabilitarmos os threads virtuais no arquivo
application.properties
e tentarmos novamente, leremos:1 Stack trace's last line: java.base/java.lang.Thread.run(Thread.java:1583) from POST /account
Desta vez, estamos usando uma instância
java.lang.Thread
clássica para lidar com nossa consulta.Threads virtuais e programação reativa não são inimigas fatais. A verdade está muito longe disso.
A combinação das vantagens dos threads virtuais em relação aos threads da plataforma padrão com as melhores práticas de programação reativa abre novas fronteiras de escalabilidade, capacidade de resposta e utilização eficiente de recursos para seus aplicativos. Desapareçam, bloqueando I/Os!
O MongoDB Reactive Streams Driver está totalmente preparado para se beneficiar das otimizações de threads virtuais com o Java 21 e, como sempre, dos princípios de programação reativa e das melhores práticas.
Esperemos que esta publicação o motivou a tentar. Implemente seu cluster no MongoDB Atlas e dê uma volta norepositório .
Para obter mais orientação e suporte, e para interagir com uma comunidade ativa de desenvolvedores, acesse o Fórum do MongoDB, onde você pode encontrar ajuda, compartilhar insights e fazer perguntas. Vamos continuar ultrapassando os limites do desenvolvimento Java juntos!
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.