Java 21 ships virtual threads (Project Loom). They look like regular Thread but cost ~1 KB instead of ~1 MB, are scheduled by the JVM instead of the OS, and let you write blocking-style code without paying for it. Some patterns become obsolete; others are unchanged.
The model
Virtual threads are scheduled onto a pool of 'carrier' OS threads (~2× CPU cores). When a virtual thread blocks on I/O, the JVM unmounts it from the carrier and remounts when ready. From your code's perspective: blocking just works. Throughput scales to millions of concurrent virtual threads.
What becomes obsolete
CompletableFuture chaining: useful only when integrating with async APIs. Reactor / RxJava: less needed unless you want backpressure and operators. Thread pools sized for I/O: just use Executors.newVirtualThreadPerTaskExecutor().
What stays the same
CPU-bound work: virtual threads don't help. Use a parallel stream or ForkJoinPool. Synchronized blocks: pin the virtual thread to its carrier, defeating the model. Use ReentrantLock instead. JNI calls: same pinning issue. Profile for pinning with -Djdk.tracePinnedThreads=full.
Sample server pattern
var executor = Executors.newVirtualThreadPerTaskExecutor();
while (running) {
Socket client = serverSocket.accept();
executor.submit(() -> {
// blocking I/O on virtual thread — costs nothing
try (var in = client.getInputStream()) {
handleRequest(in);
}
});
}Performance reality
For I/O-bound workloads (typical web apps): 5-10× throughput improvement, same code. For CPU-bound: no improvement, sometimes worse due to scheduling overhead. Benchmark before claiming numbers — early adopters hit the synchronized-block pitfall and saw regressions.