Traditional Java threads (now called "platform threads") map one-to-one onto operating system threads, which are genuinely expensive to create and hold in memory — a server can typically sustain only a few thousand at once. Virtual threads, finalized in Java 21 after years of development under Project Loom, are managed entirely by the JVM and are dramatically cheaper: a single application can run millions of them concurrently. Critically, virtual threads use the exact same Thread API Java code has always used — existing thread-per-request code often gains this benefit with minimal changes, rather than requiring a rewrite around a new async programming model.
Creating a virtual thread
Starting 100,000 platform threads simultaneously would exhaust most machines' memory and OS thread limits; the identical code with virtual threads runs comfortably, since each one is dramatically cheaper.
Thread thread = Thread.ofVirtual().start(() -> {
System.out.println("Running on a virtual thread");
});
thread.join();
// running 100,000 concurrent tasks — impractical with platform threads, fine with virtual ones
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return null;
});
}
}Why this matters: I/O-bound workloads specifically
Virtual threads shine specifically for I/O-bound work — a web server handling thousands of concurrent requests that each spend most of their time waiting on a database or network call. The JVM automatically "parks" a virtual thread during a blocking I/O call, freeing the underlying platform thread (called a "carrier thread") to run other virtual threads in the meantime, then resumes it when the I/O completes — all transparent to the code, which still just calls a normal blocking method.
Existing thread-per-request code, mostly unchanged
Because virtual threads implement the same java.lang.Thread API, code written in the traditional "one thread handles one request, blocking calls are fine" style — rather than a reactive or async style — often gains this scalability improvement by switching the thread-creation mechanism, without restructuring the application's actual logic around callbacks or reactive streams.