Understanding Java Virtual Threads: Lightweight Concurrency in Java 21
One of the most significant additions to the Java platform in recent years is virtual threads, finalized in Java 21 as part of Project Loom (JEP 444). Virtual threads dramatically simplify writing high-throughput concurrent applications. In this post, we'll explore what they are, why they matter, and how to use them.
The Problem with Platform Threads
Traditionally, each Java thread (a platform thread) maps directly to an operating system thread. OS threads are expensive:
- Each consumes around 1 MB of stack memory by default.
- Context switching has meaningful overhead.
- The number you can create is limited (typically a few thousand).
This forced developers into complex asynchronous, reactive, or thread-pool-based programming models to achieve scalability.
Enter Virtual Threads
Virtual threads are lightweight threads managed by the JVM rather than the OS. Millions of them can run concurrently. When a virtual thread blocks on I/O, the JVM unmounts it from its carrier (platform) thread, freeing that carrier to run other virtual threads.
java
// Creating a virtual thread
Thread vThread = Thread.ofVirtual().start(() -> {
System.out.println("Running in a virtual thread!");
});
vThread.join();
Using an Executor
The most practical way to leverage virtual threads is through the new executor:
java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int taskId = i;
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
System.out.println("Task " + taskId + " complete");
return taskId;
});
}
} // executor.close() waits for all tasks
Spawning 10,000 tasks like this with platform threads would likely exhaust system resources. With virtual threads, it runs comfortably.
Key Benefits
- Simplicity — Write straightforward, blocking, synchronous code that scales.
-
Compatibility — Virtual threads implement
Thread, so existing APIs work. - Efficiency — Blocking on I/O no longer wastes an OS thread.
Best Practices
- Don't pool virtual threads. They're cheap; create a new one per task.
-
Avoid
synchronizedon hot paths. Blocking inside asynchronizedblock can pin the carrier thread. PreferReentrantLock. - Use them for I/O-bound work, not CPU-bound computation, where platform threads remain appropriate.
Conclusion
Virtual threads let you write simple, imperative code that scales to massive concurrency without adopting a reactive framework. If you're on Java 21 or later, they're worth adopting for server applications that handle many concurrent requests.
Give them a try in your next Spring Boot project — as of Spring Boot 3.2, you can enable virtual threads with a single property: spring.threads.virtual.enabled=true.
Top comments (0)