Understanding Java Virtual Threads: A Practical Guide
Java 21 introduced one of the most significant changes to the concurrency model in years: virtual threads (JEP 444). If you've ever struggled with thread pool tuning or the overhead of platform threads, this feature is a game changer.
What Are Virtual Threads?
Traditional Java threads (platform threads) are thin wrappers around operating system threads. Each one consumes significant memory (typically ~1MB of stack space) and creating thousands of them is expensive.
Virtual threads are lightweight threads managed by the JVM rather than the OS. You can create millions of them without exhausting system resources, because many virtual threads are multiplexed onto a small number of carrier (platform) threads.
Creating Virtual Threads
The simplest way to start a virtual thread:
java
Thread.startVirtualThread(() -> {
System.out.println("Running in a virtual thread!");
});
For more control, use the builder API:
java
Thread vThread = Thread.ofVirtual()
.name("worker-", 0)
.start(() -> doWork());
vThread.join();
Using an Executor
The recommended approach for most applications is the new virtual-thread-per-task executor:
java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // executor.close() waits for all tasks to complete
This code spins up 10,000 concurrent tasks with minimal overhead—something that would be impractical with platform threads.
Why This Matters
The classic problem with blocking I/O is that a blocked platform thread ties up an OS thread doing nothing. With virtual threads, when a thread blocks on I/O, the JVM unmounts it from its carrier thread, freeing that carrier to run other virtual threads.
This means you can write simple, blocking-style code and still achieve the scalability that previously required complex reactive frameworks.
java
// Simple, readable, and scalable
void handleRequest(Socket socket) {
try (var in = socket.getInputStream()) {
byte[] data = in.readAllBytes(); // blocks the virtual thread, not the OS thread
process(data);
}
}
Best Practices
- Don't pool virtual threads. They are cheap to create—create a new one per task instead.
-
Avoid synchronized blocks around I/O. They can pin the virtual thread to its carrier. Prefer
ReentrantLock. - Use them for I/O-bound work. Virtual threads shine with blocking I/O, not CPU-intensive computation.
- Watch out for ThreadLocal. With millions of threads, heavy ThreadLocal usage can increase memory pressure.
Conclusion
Virtual threads let you write straightforward, blocking code that scales to massive concurrency. They lower the barrier to building high-throughput server applications without the cognitive overhead of asynchronous programming. If you're on Java 21 or later, it's worth revisiting your concurrency strategy.
Top comments (0)