Understanding Java Virtual Threads: Lightweight Concurrency in Java 21
Java 21 introduced one of the most significant additions to the platform in years: virtual threads (Project Loom). If you've ever struggled with the complexity of asynchronous programming or hit scalability walls with traditional threads, virtual threads are a game changer.
The Problem with Platform Threads
Traditional Java threads (now called platform threads) map directly to operating system threads. Each one consumes a significant chunk of memory (typically ~1MB for the stack) and switching between them involves the OS scheduler.
This creates a hard ceiling. A typical server can only handle a few thousand platform threads before running out of memory:
java
// This will likely crash or slow to a crawl
for (int i = 0; i < 100_000; i++) {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
Enter Virtual Threads
Virtual threads are lightweight threads managed by the JVM, not the OS. Thousands—even millions—can run concurrently because they're cheap to create and don't tie up an OS thread while blocked.
java
// This runs comfortably
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
Creating Virtual Threads Directly
java
// Start a single virtual thread
Thread.startVirtualThread(() -> {
System.out.println("Running in a virtual thread!");
});
// Using the builder API
Thread vThread = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() -> System.out.println("Named virtual thread"));
How It Works
When a virtual thread hits a blocking operation (like I/O), the JVM unmounts it from its carrier (platform) thread, freeing that OS thread to run other virtual threads. When the operation completes, the virtual thread is remounted to continue.
The magic is that your code stays synchronous and readable—no callbacks, no reactive chains, no CompletableFuture spaghetti.
Best Practices
- Don't pool virtual threads. They're cheap—create a new one per task instead of reusing them.
-
Avoid
synchronizedblocks around blocking calls. These can pin the virtual thread to its carrier. PreferReentrantLockinstead. - Use them for I/O-bound work, not CPU-bound tasks where platform threads still shine.
java
// Prefer this over synchronized for blocking sections
private final ReentrantLock lock = new ReentrantLock();
void safeOperation() {
lock.lock();
try {
performBlockingIO();
} finally {
lock.unlock();
}
}
Virtual Threads in Spring Boot
Spring Boot 3.2+ supports virtual threads with a single property:
properties
spring.threads.virtual.enabled=true
This makes Tomcat, task executors, and scheduled tasks use virtual threads automatically—instantly improving throughput for I/O-heavy applications.
Conclusion
Virtual threads let you write simple, blocking-style code that scales like reactive systems. For the vast majority of server applications handling many concurrent I/O operations, they offer a dramatic simplification without sacrificing performance. If you're on Java 21 or later, it's time to give them a try.
Top comments (0)