How to Optimize Java Performance With Virtual Threads, Reactive Programming, and MongoDB
Rate this article
When I first heard about Project Loom and virtual threads, my first thought was that this was a death sentence for
reactive programming. It wasn't bad news at first because reactive programming comes with its additional layer of
complexity and using imperative programming without wasting resources was music to my ears.
But I was actually wrong and a bit more reading and learning helped me understand why thinking this was a mistake.
In this post, we'll explore virtual threads and reactive programming, their differences, and how we can leverage both in
the same project to achieve peak concurrency performance in Java.
In traditional Java concurrency, threads are heavyweight entities managed by the operating system. Each OS
thread is wrapped by a platform thread which is managed by the Java Virtual Machine (JVM) that executes the Java code.
Each thread requires significant system resources, leading to limitations in scalability when dealing with a
large number of concurrent tasks. Context switching between threads is also resource-intensive and can deteriorate the
performance.
Virtual threads, introduced by Project Loom in JEP 444, are lightweight by
design and aim to overcome the limitations of traditional threads and create high-throughput concurrent applications.
They implement
java.lang.Thread
and they are managed by the JVM. Several of them can
run on the same platform thread, making them more efficient to work with a large number of small concurrent tasks.Virtual threads allow the Java developer to use the system resources more efficiently and non-blocking I/O.
But with the closely related JEP 453: Structured Concurrency and JEP 446: Scoped Values,
virtual threads also support structured concurrency to treat a group of related tasks as a single unit of work and
divide a task into smaller independent subtasks to improve response time and throughput.
Here is a basic Java example.
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 }
Output of this program:
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
We can see that the tasks ran in parallel — each in a different virtual thread, managed by a single
ForkJoinPool
and
its associated workers.First of all, reactive programming is a programming paradigm whereas virtual threads
are "just" a technical solution. Reactive programming revolves around asynchronous and event-driven programming
principles, offering solutions to manage streams of data and asynchronous operations efficiently.
The pillars of reactive programming are:
- Non-blocking I/O.
- Stream-based asynchronous communication.
- Back-pressure handling to prevent overwhelming downstream components with more data than they can handle.
The only common point of interest with virtual threads is the first one: non-blocking I/O.
The main frameworks in Java that follow the reactive programming principles are:
- Reactive Streams: provides a standard for asynchronous stream processing with non-blocking back pressure.
- Project Reactor: foundation of the reactive stack in the Spring ecosystem.
MongoDB also offers an implementation of the Reactive Streams API:
the MongoDB Reactive Streams Driver.
Here is an example where I insert a document in MongoDB and then retrieve it.
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 }
Note: The
SubscriberHelpers.OperationSubscriber
and SubscriberHelpers.PrintDocumentSubscriber
classes come from
the Reactive Streams Quick Start Primer.
You can find
the SubscriberHelpers.java
in the MongoDB Java Driver repository code examples.As you might have understood, virtual threads and reactive programming aren't competing against each other, and they
certainly agree on one thing: Blocking I/O operations is evil!
Who said that we had to make a choice? Why not use them both to achieve peak performance and prevent blocking I/Os once
and for all?
Good news: The
reactor-core
library added virtual threads support in 3.6.0. Project Reactor
is the library that provides a rich and functional implementation of Reactive Streams APIs
in Spring Boot
and WebFlux.This means that we can use virtual threads in a Spring Boot project that is using MongoDB Reactive Streams Driver and
Webflux.
There are a few conditions though:
- Activate virtual threads:
spring.threads.virtual.enabled=true
inapplication.properties
.
In the repository, my colleague Wen Jie Teo and I
updated the
pom.xml
and application.properties
so we could use virtual threads in this reactive project.You can run the following commands to get this project running quickly and test that it's running with virtual threads
correctly. You can get more details in the
README.md file but here is the gist.
Here are the instructions in English:
- Clone the repository and access the folder.
- Update the log level in
application.properties
toinfo
. - Run the
setup.js
script to initialize theaccounts
collection. - Start the Java application.
- Test one of the APIs available.
Here are the instructions translated into Bash.
First 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
Note: On macOS, you may have to use
sed -i '' 's/warn/info/g' src/main/resources/application.properties
if you are not using gnu-sed
, or you can just edit the file manually.Second terminal
1 curl 'localhost:8080/account' -H 'Content-Type: application/json' -d '{"accountNum": "1"}'
If everything worked as planned, you should see this line in the first terminal (where you are running Spring).
1 Stack trace's last line: java.base/java.lang.VirtualThread.run(VirtualThread.java:309) from POST /account
This is the last line in the stack trace that we are logging. It proves that we are using virtual threads to handle
our query.
If we disable the virtual threads in the
application.properties
file and try again, we'll read instead:1 Stack trace's last line: java.base/java.lang.Thread.run(Thread.java:1583) from POST /account
This time, we are using a classic
java.lang.Thread
instance to handle our query.Virtual threads and reactive programming are not mortal enemies. The truth is actually far from that.
The combination of virtual threads’ advantages over standard platform threads with the best practices of reactive
programming opens up new frontiers of scalability, responsiveness, and efficient resource utilization for your
applications. Be gone, blocking I/Os!
MongoDB Reactive Streams Driver is fully equipped to
benefit from both virtual threads optimizations with Java 21, and — as always — benefit from the reactive programming
principles and best practices.
I hope this post motivated you to give it a try. Deploy your cluster on
MongoDB Atlas and give the
repository a spin.
For further guidance and support, and to engage with a vibrant community of developers, head over to the
MongoDB Forum where you can find help, share insights, and ask those
burning questions. Let's continue pushing the boundaries of Java development together!
Top Comments in Forums
There are no comments on this article yet.