Understanding Java Virtual Threads: Lightweight Concurrency in Modern Java
Java 21 introduced one of the most significant changes to the platform's concurrency model in years: virtual threads, delivered as part of Project Loom (JEP 444). If you've ever struggled with thread pool tuning or hit scalability walls with blocking I/O, virtual threads are worth understanding.
The Problem with Platform Threads
Traditional Java threads (now called platform threads) are thin wrappers around operating system threads. They are expensive:
- Each thread consumes around 1 MB of stack memory by default.
- The OS scheduler manages context switching, which adds overhead.
- A typical machine can only support a few thousand concurrent platform threads before running into resource limits.
This led to the popularity of asynchronous, reactive programming styles that avoid blocking. But reactive code is notoriously hard to read, debug, and maintain.
Enter Virtual Threads
Virtual threads are lightweight threads managed by the JVM rather than the operating system. Millions of them can run concurrently, and they are mapped onto a small pool of platform threads (called carrier threads).
When a virtual thread blocks on I/O, the JVM automatically unmounts it from its carrier thread, freeing that carrier to run other virtual threads.
Creating Virtual Threads
The API is refreshingly simple:
java
// Start a single virtual thread
Thread.startVirtualThread(() -> {
System.out.println("Running in a virtual thread");
});
// Using a builder
Thread vThread = Thread.ofVirtual()
.name("worker-1")
.start(() -> doWork());
The real power shows up with executors:
java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
This code spawns 10,000 concurrent tasks. With platform threads this would likely exhaust system resources; with virtual threads it runs comfortably.
Writing Simple, Blocking Code Again
The biggest win is that you can write straightforward, synchronous-looking code that still scales:
java
var response = httpClient.send(request, BodyHandlers.ofString());
process(response.body());
The blocking call no longer wastes a precious OS thread. The scheduler handles the rest.
Best Practices
-
Don't pool virtual threads. They are cheap to create. Use
newVirtualThreadPerTaskExecutor()instead of a fixed pool. -
Avoid
synchronizedblocks around blocking I/O. They can pin the virtual thread to its carrier. PreferReentrantLock. - Use them for I/O-bound tasks. For CPU-bound work, platform threads still make sense.
Conclusion
Virtual threads let you keep the simplicity of blocking code while achieving the scalability once reserved for reactive frameworks. Combined with frameworks like Spring Boot, which now support virtual threads out of the box, they represent a meaningful step forward for Java server-side development.
Try them in your next project and measure the difference for yourself.
Top comments (0)