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

One of the most significant additions to the Java platform in recent years is virtual threads, finalized in Java 21 as part of Project Loom (JEP 444). Virtual threads dramatically simplify writing high-throughput concurrent applications. In this post, we'll explore what they are, why they matter, and how to use them.

The Problem with Platform Threads

Traditionally, each Java thread (a platform thread) maps directly to an operating system thread. OS threads are expensive:

  • Each consumes around 1 MB of stack memory by default.
  • Context switching has meaningful overhead.
  • The number you can create is limited (typically a few thousand).

This forced developers into complex asynchronous, reactive, or thread-pool-based programming models to achieve scalability.

Enter Virtual Threads

Virtual threads are lightweight threads managed by the JVM rather than the OS. Millions of them can run concurrently. When a virtual thread blocks on I/O, the JVM unmounts it from its carrier (platform) thread, freeing that carrier to run other virtual threads.

java
// Creating a virtual thread
Thread vThread = Thread.ofVirtual().start(() -> {
System.out.println("Running in a virtual thread!");
});
vThread.join();

Using an Executor

The most practical way to leverage virtual threads is through the new executor:

java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int taskId = i;
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
System.out.println("Task " + taskId + " complete");
return taskId;
});
}
} // executor.close() waits for all tasks

Spawning 10,000 tasks like this with platform threads would likely exhaust system resources. With virtual threads, it runs comfortably.

Key Benefits

  1. Simplicity — Write straightforward, blocking, synchronous code that scales.
  2. Compatibility — Virtual threads implement Thread, so existing APIs work.
  3. Efficiency — Blocking on I/O no longer wastes an OS thread.

Best Practices

  • Don't pool virtual threads. They're cheap; create a new one per task.
  • Avoid synchronized on hot paths. Blocking inside a synchronized block can pin the carrier thread. Prefer ReentrantLock.
  • Use them for I/O-bound work, not CPU-bound computation, where platform threads remain appropriate.

Conclusion

Virtual threads let you write simple, imperative code that scales to massive concurrency without adopting a reactive framework. If you're on Java 21 or later, they're worth adopting for server applications that handle many concurrent requests.

Give them a try in your next Spring Boot project — as of Spring Boot 3.2, you can enable virtual threads with a single property: spring.threads.virtual.enabled=true.

Top comments (0)