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.

Advertisement

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().

Advertisement

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.

Virtual threads + blocking I/O = win. Watch for synchronized-block pinning. CPU-bound still needs careful pool sizing.