Understanding Java's Virtual Threads: Lightweight Concurrency in Action
Java 21 introduced virtual threads as a stable feature (JEP 444), fundamentally changing how we approach concurrency on the JVM. In this post, we'll explore what virtual threads are, why they matter, and how to use them effectively.
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 involves the OS scheduler for context switching. This makes them expensive:
java
// Creating thousands of platform threads is costly
for (int i = 0; i < 10_000; i++) {
new Thread(() -> {
// blocking I/O ties up an OS thread
processRequest();
}).start();
}
In high-throughput server applications, the classic "thread-per-request" model hits a ceiling because you simply cannot create enough OS threads.
Enter Virtual Threads
Virtual threads are managed by the JVM rather than the OS. Many virtual threads run on a small pool of carrier platform threads. When a virtual thread blocks (e.g., on I/O), the JVM detaches it from its carrier, freeing that carrier to run other virtual threads.
java
// Creating a million virtual threads is perfectly fine
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
Key Benefits
- Cheap creation: Virtual threads start with a tiny stack that grows on demand.
- Familiar model: You write straightforward blocking code—no callbacks or reactive chains.
- Better scalability: Throughput is limited by resources, not thread count.
Using Virtual Threads in Spring Boot
As of Spring Boot 3.2, enabling virtual threads is a one-line configuration change:
properties
spring.threads.virtual.enabled=true
This makes Tomcat handle each request on a virtual thread, allowing your application to serve many concurrent blocking requests without exhausting the thread pool.
You can also create virtual threads explicitly:
java
Thread vThread = Thread.ofVirtual()
.name("worker-", 0)
.start(() -> System.out.println("Running on " + Thread.currentThread()));
vThread.join();
Pitfalls to Watch For
-
Pinning: When a virtual thread runs inside a
synchronizedblock during a blocking call, it stays pinned to its carrier. PreferReentrantLockfor blocking sections. - Don't pool them: Virtual threads are meant to be created per-task, not pooled and reused.
- CPU-bound work: Virtual threads shine for I/O-bound tasks; they offer little benefit for pure CPU computation.
Conclusion
Virtual threads let you write simple, blocking code that scales to millions of concurrent tasks. Combined with Spring Boot's seamless integration, they offer a compelling upgrade path for modern server applications—no reactive rewrite required.
Give them a try in your next project and enjoy readable concurrency at scale.
Top comments (0)