Concurrency - Part 5
Before virtual threads, if you wanted to write scalable concurrent code without blocking physical OS threads, you typically chose one of these approaches:
- CompletableFuture — for async, non-blocking computation using futures and chaining.
- ExecutorService with thread pools — for managing a fixed number of threads efficiently.
- Reactive frameworks like Reactor or RxJava — for highly scalable, event-driven applications with non-blocking flows.
Each of these had its own learning curve and trade-offs. In Java 21 (this PR), virtual threads changed the game by allowing:
- Blocking code (like
Thread.sleep()
orInputStream.read()
) - Written in a straightforward, linear style
- While still scaling to thousands of threads (because they're lightweight)
Clean, Readable Code
Virtual threads let you write code in a familiar, blocking style — without managing callbacks or chaining. Less boilerplate. More clarity.
// With CompletableFuture
CompletableFuture.supplyAsync(() -> getData())
.thenApply(data -> processData(data))
.thenAccept(System.out::println);
// With Virtual Threads
var result = getData();
var processed = processData(result);
System.out.println(processed);
Parallel with Node.js Async/Await
If you’re coming from a Node.js background, virtual threads in Java will feel similar to async/await — both enable writing asynchronous code that looks synchronous.
Node.js with async/await:
Before async/await:
Just like async/await replaced callback hell in JavaScript, virtual threads eliminate the need for deeply nested CompletableFuture
chains in Java. You retain the readability of sequential code, while the underlying runtime handles scheduling, blocking, and continuation behind the scenes.
⚠️ Note: Virtual threads do real blocking, but very efficiently using JVM scheduling.
async/await
, on the other hand, is syntactic sugar over non-blocking Promises. The underlying model is different even if the surface syntax looks similar.
Better Resource Management
- Virtual threads are lightweight and managed by the JVM — you can create millions of them.
CompletableFuture
relies on a fixed thread pool, which can become a bottleneck under heavy load.
JVM-Level Behavior of Virtual Threads
Unlike platform threads (which are heavyweight OS threads), virtual threads are scheduled by the JVM, not the OS. This enables a large number of virtual threads (up to a million) to coexist.
- Platform Thread (PT): \~0.25–1 MB of memory; JVM can manage \~1,000–10,000
- Virtual Thread (VT): \~1–2 KB of memory; JVM can manage up to 1 million
Here’s how the JVM handles virtual threads internally:
- A virtual thread runs on a platform thread (PT) until it hits a blocking operation (e.g., DB call, file I/O).
- When blocked, the JVM unmounts the virtual thread from the PT.
- The PT is now free to run another virtual thread.
- Once the result is ready, the virtual thread is remounted on an available PT and resumes execution.
This mount/unmount mechanism ensures that the few available platform threads are always busy with active tasks, minimizing wasted resources.
Easier Debugging and Tracing
With virtual threads, you get:
- Clean stack traces
- Compatibility with traditional debugging tools
- Easier step-through in IDEs
As opposed to tracing through deeply chained async calls in CompletableFuture
.
Quick Demo
public class VirtualThreadExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() ->
System.out.println("Running in: " + Thread.currentThread().getName())
);
thread.join(); // Wait for the virtual thread to complete
}
}
//output
Running in: my-virtual-thread
Virtual Threads in Web Servers (Spring Boot / Tomcat)
Starting with Spring Boot 3.2+, you can enable virtual threads in embedded Tomcat using:
This allows your application to handle incoming web requests using virtual threads, greatly increasing concurrency without thread pool exhaustion.
How it works:
- Tomcat accepts web requests and spawns virtual threads instead of blocking platform threads.
- These virtual threads call into your Spring-based business logic.
- Calls to storage (e.g., databases) can block without worrying about holding up platform threads.
- The JVM automatically manages scheduling behind the scenes.
This design improves scalability — particularly for I/O-heavy workloads — without changing your controller or service code.
⚠️ Note: While enabling virtual threads in Tomcat is powerful, it’s worth proceeding cautiously — some libraries, tools, or native code may not yet fully support or benefit from the virtual thread model.
Final Thoughts
Virtual threads are a major step forward in Java’s concurrency model. They combine the simplicity of synchronous code with the scalability of asynchronous processing — all without the boilerplate and complexity of CompletableFuture
.
They’re not a silver bullet, but for many applications, virtual threads are a cleaner, more powerful alternative.
Happy coding! 💻