Understanding Java Virtual Threads: A Practical Guide
Java 21 introduced one of the most significant additions to the platform in years: virtual threads, delivered as part of Project Loom (JEP 444). If you've ever struggled with the scalability limits of traditional thread-per-request models, this feature is a game changer.
The Problem with Platform Threads
Traditional Java threads (now called platform threads) are thin wrappers around operating system threads. They're expensive:
- Each thread consumes ~1MB of stack memory by default
- The OS scheduler manages context switching
- You can realistically create only a few thousand before running out of resources
This forced developers toward complex asynchronous, reactive programming models to achieve high concurrency.
Enter Virtual Threads
Virtual threads are lightweight threads managed by the JVM rather than the OS. You can create millions of them without exhausting system resources.
java
// Creating a single virtual thread
Thread.startVirtualThread(() -> {
System.out.println("Running in a virtual thread!");
});
// Using an ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return "done";
});
}
}
How They Work
Virtual threads are mounted onto a small pool of platform threads called carrier threads. When a virtual thread hits a blocking operation (like I/O), the JVM unmounts it from its carrier, freeing that carrier to run other virtual threads.
Virtual Threads (millions)
|
[ mounted on ]
|
Carrier Threads (few, = CPU cores)
|
OS Threads
Writing Scalable Code
The beauty is that you write simple, blocking, synchronous code and get asynchronous scalability for free:
java
void handleRequest(Socket socket) {
try (var in = socket.getInputStream();
var out = socket.getOutputStream()) {
byte[] data = in.readAllBytes(); // blocking is fine!
out.write(process(data));
}
}
Best Practices
- Don't pool virtual threads — create a new one per task. They're cheap.
-
Avoid
synchronizedblocks around blocking calls — they can pin the virtual thread to its carrier. UseReentrantLockinstead. - Don't rely on thread-local variables heavily — with millions of threads, this can bloat memory.
Spring Boot Support
Spring Boot 3.2+ makes enabling virtual threads trivial:
properties
spring.threads.virtual.enabled=true
With this single property, Tomcat and other components will process requests on virtual threads, dramatically improving throughput for I/O-bound workloads.
Conclusion
Virtual threads let you keep the simplicity of imperative, blocking code while achieving the scalability that previously required reactive frameworks. For most server applications dominated by I/O, this is a clear win. Give them a try in your next Java 21 project!
Top comments (0)