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). If you've ever struggled with the scalability limits of traditional thread-per-request models, this feature is a game changer.

The Problem with Platform Threads

Traditional Java threads (now called platform threads) are thin wrappers around operating system threads. They're expensive:

  • Each thread consumes ~1MB of stack memory by default
  • The OS scheduler manages context switching
  • You can realistically create only a few thousand before running out of resources

This 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. You can create millions of them without exhausting system resources.

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

How They Work

Virtual threads are mounted onto a small pool of platform threads called carrier threads. When a virtual thread hits a blocking operation (like I/O), the JVM unmounts it from its carrier, freeing that carrier to run other virtual threads.

Virtual Threads (millions)
|
[ mounted on ]
|
Carrier Threads (few, = CPU cores)
|
OS Threads

Writing Scalable Code

The beauty is that you write simple, blocking, synchronous code and get asynchronous scalability for free:

java
void handleRequest(Socket socket) {
try (var in = socket.getInputStream();
var out = socket.getOutputStream()) {
byte[] data = in.readAllBytes(); // blocking is fine!
out.write(process(data));
}
}

Best Practices

  1. Don't pool virtual threads — create a new one per task. They're cheap.
  2. Avoid synchronized blocks around blocking calls — they can pin the virtual thread to its carrier. Use ReentrantLock instead.
  3. Don't rely on thread-local variables heavily — with millions of threads, this can bloat memory.

Spring Boot Support

Spring Boot 3.2+ makes enabling virtual threads trivial:

properties
spring.threads.virtual.enabled=true

With this single property, Tomcat and other components will process requests on virtual threads, dramatically improving throughput for I/O-bound workloads.

Conclusion

Virtual threads let you keep the simplicity of imperative, blocking code while achieving the scalability that previously required reactive frameworks. For most server applications dominated by I/O, this is a clear win. Give them a try in your next Java 21 project!

Top comments (0)