Q32 of 40 · Core Java

What are virtual threads (Java 21) and how do they differ from platform threads?

Core JavaSeniorvirtual-threadsjava-21project-loomconcurrencyperformancejvm

Short answer

Short answer: Virtual threads (JEP 444, Java 21) are lightweight JVM-managed threads that are multiplexed onto a small pool of OS platform threads (carrier threads). Unlike platform threads which map 1:1 to OS threads, virtual threads are cheap to create (millions per JVM), cheap to block (the carrier thread is released when a virtual thread blocks on I/O), and require no thread pool sizing tuning.

Detail

Platform threads vs virtual threads

Platform thread Virtual thread
Backed by OS thread JVM continuation
Creation cost ~1 MB stack reservation ~few KB
Blocking I/O OS thread blocked Carrier released, virtual thread parked
Pool sizing Manual tuning required Let JVM manage
Synchronised block Pins carrier thread Pinning issue (JEP 491 in Java 24 fixes)
import java.util.concurrent.Executors;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;

// Old style — fixed platform thread pool, tuned by hand
var pool = Executors.newFixedThreadPool(200);

// Virtual threads — one virtual thread per task, JVM handles multiplexing
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (var endpoint : endpoints) {
        executor.submit(() -> {
            // Blocking HTTP call — carrier thread is released while waiting
            var resp = HttpClient.newHttpClient().send(
                HttpRequest.newBuilder(URI.create(endpoint)).build(),
                HttpResponse.BodyHandlers.ofString());
            return resp.body();
        });
    }
} // auto-shutdown on try-with-resources exit

When virtual threads win I/O-bound workloads: many concurrent HTTP calls, DB queries, file reads. The JVM parks the virtual thread during the wait and lets other virtual threads run on the same carrier thread. Throughput scales with concurrency, not with OS thread count.

When virtual threads don't help CPU-bound tasks (hashing, compression, number crunching) — the virtual thread still needs a carrier for the full duration. Use a bounded platform thread pool or ForkJoinPool for CPU work.

Pinning caveat (Java 21–23) A virtual thread "pins" its carrier while inside a synchronized block or native call. Heavy use of synchronized (e.g., legacy JDBC drivers) can exhaust carriers. JEP 491 (Java 24) removes most pinning by making synchronized virtual-thread-aware.

QA angle: API test harnesses making thousands of parallel requests are the classic virtual-thread use case — replace a 200-thread pool with newVirtualThreadPerTaskExecutor and let the JVM handle the rest.

// WHAT INTERVIEWERS LOOK FOR

The 1:1 OS-thread mapping of platform threads vs M:N multiplexing of virtual threads. The pinning issue with synchronized blocks. Knowing when to use virtual threads (I/O-bound) vs thread pools (CPU-bound). Strong answers mention Project Loom, JEP 444, and structured concurrency (JEP 453).

// COMMON PITFALL

Using virtual threads for CPU-bound tasks expecting a speedup. CPU work can't benefit from the park/unpark cycle — virtual threads only help when threads would otherwise be blocked waiting.