Q32 of 40 · Core Java
What are virtual threads (Java 21) and how do they differ from platform threads?
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.