DEV Community

Said Olano
Said Olano

Posted on

Understanding Java Virtual Threads: Lightweight Concurrency in Java 21

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, delivered as part of Project Loom (JEP 444). This feature fundamentally changes how we write high-throughput concurrent applications in Java.

The Problem with Platform Threads

Traditionally, each Java thread maps directly to an operating system thread. These platform threads are expensive:

  • Each thread consumes roughly 1MB of stack memory.
  • Context switching is handled by the OS and carries overhead.
  • The number of threads is limited (typically a few thousand).

For I/O-bound applications like web servers, this becomes a bottleneck. You end up either blocking threads (wasting resources) or adopting complex asynchronous, reactive programming models.

Enter Virtual Threads

Virtual threads are lightweight threads managed by the JVM rather than the OS. Millions of them can run on a small pool of platform threads (called carrier threads).

java
// Creating a virtual thread
Thread.startVirtualThread(() -> {
System.out.println("Running in a virtual thread!");
});

// Using an ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return null;
});
}
}

The key insight: when a virtual thread blocks on I/O, the JVM unmounts it from its carrier thread, freeing that carrier to run other virtual threads. When the I/O completes, the virtual thread is remounted and resumes.

Writing Simple, Blocking Code Again

The biggest win is that you can write straightforward, blocking code that scales:

java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future user = executor.submit(() -> fetchUser());
Future order = executor.submit(() -> fetchOrder());
String result = user.get() + " | " + order.get();
System.out.println(result);
}

No callbacks, no reactive chains—just clean, readable code.

Spring Boot Integration

Spring Boot 3.2+ supports virtual threads with a single property:

properties
spring.threads.virtual.enabled=true

With this enabled, Tomcat serves each request on a virtual thread, dramatically improving throughput for I/O-heavy endpoints without any code changes.

Pitfalls to Watch For

  1. Pinning: Using synchronized blocks around blocking calls can pin a virtual thread to its carrier. Prefer ReentrantLock instead.
  2. Thread pools: Don't pool virtual threads—they're cheap to create. Use newVirtualThreadPerTaskExecutor().
  3. ThreadLocals: Be cautious with heavy ThreadLocal usage since you may now have millions of threads.

Conclusion

Virtual threads let Java developers write simple, synchronous code that scales to massive concurrency levels. Combined with framework support in Spring Boot, they offer a compelling alternative to reactive programming for many use cases. If you're on Java 21 or later, it's time to give them a try.

Top comments (0)