Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
Java
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
Javachevron-right

How to Optimize Java Performance With Virtual Threads, Reactive Programming, and MongoDB

Maxime Beugnet5 min read • Published Aug 29, 2024 • Updated Aug 29, 2024
SpringMongoDBJava
Facebook Icontwitter iconlinkedin icon
Rate this article
star-empty
star-empty
star-empty
star-empty
star-empty

Introduction

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.
Learn more about virtual threads support with MongoDB in my previous post on this topic.

Virtual threads

Traditional thread model 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.

Introducing virtual threads

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.

Benefits of virtual threads

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.

Example

Here is a basic Java example.
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3
4public 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:
1Task 6 executed by virtual thread: VirtualThread[#35]/runnable@ForkJoinPool-1-worker-6
2Task 2 executed by virtual thread: VirtualThread[#31]/runnable@ForkJoinPool-1-worker-2
3Task 10 executed by virtual thread: VirtualThread[#39]/runnable@ForkJoinPool-1-worker-10
4Task 1 executed by virtual thread: VirtualThread[#29]/runnable@ForkJoinPool-1-worker-1
5Task 5 executed by virtual thread: VirtualThread[#34]/runnable@ForkJoinPool-1-worker-5
6Task 7 executed by virtual thread: VirtualThread[#36]/runnable@ForkJoinPool-1-worker-7
7Task 4 executed by virtual thread: VirtualThread[#33]/runnable@ForkJoinPool-1-worker-4
8Task 3 executed by virtual thread: VirtualThread[#32]/runnable@ForkJoinPool-1-worker-3
9Task 8 executed by virtual thread: VirtualThread[#37]/runnable@ForkJoinPool-1-worker-8
10Task 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.

Reactive programming

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.
In Java, reactive programming is traditionally implemented with the observer pattern.
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.

Reactive programming frameworks

The main frameworks in Java that follow the reactive programming principles are:

Example

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.
1import com.mongodb.client.result.InsertOneResult;
2import com.mongodb.quickstart.SubscriberHelpers.OperationSubscriber;
3import com.mongodb.quickstart.SubscriberHelpers.PrintDocumentSubscriber;
4import com.mongodb.reactivestreams.client.MongoClient;
5import com.mongodb.reactivestreams.client.MongoClients;
6import com.mongodb.reactivestreams.client.MongoCollection;
7import org.bson.Document;
8
9public 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.

Virtual threads and reactive programming working together

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:
  • Use Tomcat because — as I'm writing this post — Netty (used by default by Webflux) doesn't support virtual threads. See GitHub issues 12848 and 39425 for more details.
  • Activate virtual threads: spring.threads.virtual.enabled=true in application.properties.

Let's test

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 to info.
  • Start a local MongoDB single node replica set instance or use MongoDB Atlas.
  • Run the setup.js script to initialize the accounts collection.
  • Start the Java application.
  • Test one of the APIs available.
Here are the instructions translated into Bash.
First terminal:
1git clone git@github.com:mongodb-developer/mdb-spring-boot-reactive.git
2cd mdb-spring-boot-reactive/
3sed -i 's/warn/info/g' src/main/resources/application.properties
4docker run --rm -d -p 27017:27017 -h $(hostname) --name mongo mongo:latest --replSet=RS && sleep 5 && docker exec mongo mongosh --quiet --eval "rs.initiate();"
5mongosh --file setup.js
6mvn 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
1curl '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).
1Stack 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:
1Stack 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.

Conclusion

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.
Start the Conversation

Facebook Icontwitter iconlinkedin icon
Rate this article
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Tutorial

Single-Collection Designs in MongoDB with Spring Data (Part 1)


Sep 09, 2024 | 10 min read
Tutorial

Java Meets Queryable Encryption: Developing a Secure Bank Account Application


Oct 08, 2024 | 14 min read
Quickstart

Introduction to MongoDB and Helidon


Nov 12, 2024 | 6 min read
Tutorial

Using Azure Kubernetes Services for Java Spring Boot Microservices


Apr 15, 2024 | 9 min read
Table of Contents