Understanding Java's Virtual Threads: A Practical Guide to Project Loom
Java 21 introduced one of the most significant additions to the platform in years: virtual threads, delivered through Project Loom. If you've ever struggled with thread-per-request scalability limits, 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. Each one consumes roughly 1MB of stack memory and mapping is 1:1 with an OS thread. This means creating tens of thousands of them is expensive and often impractical.
java
// The old approach - limited by OS thread count
ExecutorService executor = Executors.newFixedThreadPool(200);
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
// blocking I/O ties up a precious OS thread
return fetchFromDatabase();
});
}
When a platform thread blocks on I/O, the underlying OS thread sits idle, wasting resources.
Enter Virtual Threads
Virtual threads are lightweight threads managed by the JVM, not the OS. Thousands or even millions can run concurrently. When a virtual thread blocks, it is unmounted from its carrier (platform) thread, freeing that carrier to run other work.
java
// The new approach - one virtual thread per task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return fetchFromDatabase();
});
}
}
You can also create them directly:
java
Thread vThread = Thread.ofVirtual().start(() -> {
System.out.println("Running in " + Thread.currentThread());
});
vThread.join();
Key Benefits
- Massive scalability: Millions of concurrent tasks without exhausting memory.
- Simple programming model: Write straightforward blocking code instead of complex reactive chains.
-
No API changes: Existing
ThreadandExecutorServicecode works with minimal modification.
Watch Out for Pinning
Virtual threads can become pinned to their carrier thread in certain scenarios, preventing unmounting:
java
// synchronized blocks can pin the virtual thread
synchronized (lock) {
performBlockingIO(); // carrier thread stays occupied!
}
Prefer ReentrantLock over synchronized for blocking sections:
java
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
performBlockingIO();
} finally {
lock.unlock();
}
Integration with Spring Boot
Spring Boot 3.2+ supports virtual threads out of the box. Enable them with a single property:
properties
spring.threads.virtual.enabled=true
This makes Tomcat serve each request on a virtual thread, dramatically improving throughput for I/O-bound applications without rewriting your controllers.
When to Use Them
Virtual threads shine for I/O-bound workloads with high concurrency. For CPU-bound tasks, stick with a bounded pool of platform threads, since virtual threads offer no advantage when the CPU is the bottleneck.
Conclusion
Virtual threads let you write simple, readable, blocking-style code while achieving the scalability previously reserved for reactive frameworks. As Java continues to evolve, mastering Project Loom will be essential for building high-performance backend systems.
Give them a try in your next project—your code and your servers will thank you.
Top comments (0)