Understanding Java Virtual Threads: A Practical Guide
Java 21 introduced virtual threads as a stable feature (JEP 444), fundamentally changing how we write concurrent applications. In this post, we'll explore what they are, why they matter, and how to use them effectively.
What Are Virtual Threads?
Traditional Java threads (platform threads) map directly to operating system threads. Each OS thread consumes significant memory (often 1MB+ of stack) and context-switching is expensive. This limits applications to a few thousand concurrent threads.
Virtual threads are lightweight threads managed by the JVM rather than the OS. Millions of them can run on a small pool of carrier (platform) threads.
The Problem They Solve
Consider a typical web server handling blocking I/O:
java
// Traditional approach - limited by OS threads
ExecutorService executor = Executors.newFixedThreadPool(200);
executor.submit(() -> {
var response = httpClient.send(request); // blocks the thread
process(response);
});
With only 200 threads, high I/O concurrency becomes a bottleneck. Virtual threads eliminate this constraint.
Creating Virtual Threads
java
// Create and start a single virtual thread
Thread.startVirtualThread(() -> {
System.out.println("Running in a virtual thread");
});
// Using an ExecutorService (recommended)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return "done";
});
}
} // executor.close() waits for all tasks
This code launches a million tasks without exhausting system resources.
How It Works Under the Hood
When a virtual thread hits a blocking operation (like I/O), the JVM unmounts it from its carrier thread and parks it. The carrier thread is freed to run other virtual threads. When the blocking call completes, the virtual thread is remounted and resumes execution.
Best Practices
-
Don't pool virtual threads — they're cheap to create. Use
newVirtualThreadPerTaskExecutor(). -
Avoid
synchronizedblocks for long operations, as they can pin the virtual thread to its carrier. PreferReentrantLock. - Keep ThreadLocals minimal — with millions of threads, they can consume significant memory.
java
// Prefer ReentrantLock over synchronized
private final ReentrantLock lock = new ReentrantLock();
void safeUpdate() {
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
}
Virtual Threads in Spring Boot
Spring Boot 3.2+ makes enabling virtual threads trivial:
properties
spring.threads.virtual.enabled=true
This configures Tomcat and other components to use virtual threads for request handling.
Conclusion
Virtual threads let you write simple, blocking-style code that scales like asynchronous code. They preserve the readability of synchronous programming while unlocking massive concurrency — a genuine win for backend developers.
Start experimenting with them today, and rethink whether reactive complexity is still necessary for your I/O-bound workloads.
Top comments (0)