DEV Community

Said Olano
Said Olano

Posted on

Understanding Java Virtual Threads: A Practical Guide

Understanding Java Virtual Threads: A Practical Guide

Java 21 introduced one of the most significant additions to the platform in years: virtual threads, delivered as part of Project Loom (JEP 444). In this post, we'll explore what they are, why they matter, and how to use them effectively.

The Problem with Platform Threads

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

  • Each consumes around 1MB of stack memory
  • Context switching is handled by the OS scheduler
  • Creating thousands of them can exhaust system resources

This limitation forced developers toward complex asynchronous, reactive programming models to achieve high concurrency.

Enter Virtual Threads

Virtual threads are lightweight threads managed by the JVM rather than the OS. Millions can run concurrently, because they're mounted onto a small pool of carrier platform threads only when actively executing.

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

// Using an ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}

How It Works

When a virtual thread hits a blocking operation (like I/O), the JVM unmounts it from its carrier thread, freeing that carrier to run other virtual threads. Once the blocking call completes, the virtual thread is remounted and resumed.

This means you can write simple, blocking-style code that scales like reactive code:

java
var response = httpClient.send(request, BodyHandlers.ofString());
process(response.body());

No callbacks, no CompletableFuture chains—just readable, sequential logic.

Best Practices

  1. Don't pool virtual threads. They're cheap to create; use one per task.
  2. Avoid pinning. Blocking inside synchronized blocks pins the carrier thread. Prefer ReentrantLock instead.
  3. Use them for I/O-bound work. CPU-bound tasks won't benefit from virtual threads.

java
// Prefer this
private final ReentrantLock lock = new ReentrantLock();

void safeUpdate() {
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
}

Spring Boot Integration

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

properties
spring.threads.virtual.enabled=true

This makes Tomcat and other components use virtual threads for request handling, dramatically improving throughput for I/O-heavy applications.

Conclusion

Virtual threads let you achieve massive concurrency while keeping your code simple and maintainable. For most server-side applications dominated by I/O, they're a game-changer worth adopting today.

Top comments (0)