DEV Community

Said Olano
Said Olano

Posted on

Understanding Java's Virtual Threads: A Practical Guide to Project Loom

Understanding Java's Virtual Threads: A Practical Guide to Project Loom

Java 21 introduced one of the most significant concurrency features in the language's history: virtual threads (delivered via Project Loom). If you've ever struggled with thread-pool tuning or the complexity of reactive programming, virtual threads are worth your attention.

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 requires a costly context switch managed by the OS. This means you can typically only run a few thousand of them before your application grinds to a halt.

This limitation forced developers into two uncomfortable choices:

  1. Thread pools — reuse a limited number of threads, but block on I/O.
  2. Reactive programming — non-blocking, but hard to read, debug, and maintain.

Enter Virtual Threads

Virtual threads are lightweight threads managed by the JVM rather than the OS. They are mounted onto a small pool of carrier platform threads only when running, and unmounted when they block on I/O. This lets you spin up millions of them.

java
// Creating a single 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;
});
});
}

The beauty here is that the code looks completely synchronous and blocking — yet it scales like reactive code.

Virtual Threads in Spring Boot

Spring Boot 3.2+ supports virtual threads out of the box. Enabling them for your web server is a one-line configuration change:

properties
spring.threads.virtual.enabled=true

With this enabled, each incoming HTTP request is handled on its own virtual thread. No more tuning server.tomcat.threads.max to avoid exhausting your thread pool under load.

java
@RestController
public class OrderController {

private final OrderService orderService;

public OrderController(OrderService orderService) {
    this.orderService = orderService;
}

@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable Long id) {
    // A blocking call here no longer ties up a scarce platform thread
    return orderService.findById(id);
}
Enter fullscreen mode Exit fullscreen mode

}

Gotchas to Watch For

Virtual threads are powerful, but keep these caveats in mind:

  • Pinning: When a virtual thread executes inside a synchronized block during a blocking operation, it gets pinned to its carrier thread, defeating the benefit. Prefer ReentrantLock for guarded blocking sections.
  • Don't pool them: Virtual threads are cheap. Create a new one per task instead of pooling.
  • ThreadLocal usage: With millions of threads, heavy ThreadLocal usage can increase memory pressure. Consider scoped values (a preview feature) as a lighter alternative.

When Should You Use Them?

Virtual threads shine for I/O-bound workloads — think web servers, database access, and calls to downstream services. For CPU-bound work, you still want a bounded number of platform threads matching your core count.

Conclusion

Virtual threads let you write simple, readable, blocking-style code while achieving the scalability that previously required reactive frameworks. Combined with Spring Boot's seamless integration, they represent a genuine step forward for building high-throughput Java applications. If you're on Java 21+, it's time to experiment.

Top comments (0)