Java Virtual Threads (Project Loom)
- Last updated
- Prerequisites:
- Java threading basics
- Understanding of blocking I/O and thread-per-request servers
- java
- virtual-threads
- loom
- concurrency
- threading
- jvm
Read at your depth
The practical view
Virtual threads let you write blocking-style code that scales like async code: Thread.ofVirtual().start(() -> { var r = client.get(url); process(r); }); or Executors.newVirtualThreadPerTaskExecutor(). Each virtual thread is a Java Thread instance, but it is scheduled onto a small pool of platform (carrier) threads by the JVM. When a virtual thread blocks on I/O, the JVM unmounts it from the carrier, runs other virtual threads on that carrier, and remounts it when the I/O completes. You keep normal try-with-resources, blocking calls, and stack traces — no CompletableFuture or reactive chains needed.
Legacy vs modern
Platform thread-per-request vs virtual thread-per-request
A platform thread per request is capped by OS resources; virtual threads make the thread count a heap concern, keeping blocking code readable.
ExecutorService pool = Executors.newFixedThreadPool(200);
pool.submit(() -> handle(request));try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
pool.submit(() -> handle(request));
}Interview gotchas
Context
The interviewer wants to verify that you know virtual threads are not free and understand the pinning failure mode introduced by Loom.
The mechanical answer
Pinning happens when a virtual thread, while holding a native or synchronized monitor, performs a blocking operation that the JVM cannot transparently unmount. Cases: synchronized blocks or methods during a blocking call (the monitor is tied to the carrier until released), native methods that block (JNI), and Object.wait in some paths. When pinned, the carrier cannot be reused for other virtual threads, so with many pinned threads you effectively run with as many carriers as pinned threads — the scaling benefit collapses. Java 24+ (JEP 491) removed most pinning for synchronized by making the monitor pin only when contended, but native pinning remains a documented limitation.
Trap
The naive answer is 'virtual threads never block the OS'. They do block carriers when pinned, and before JEP 491 synchronized was a common accidental cause (locks acquired around blocking I/O). Another trap: claiming pinning is a bug you can ignore — under heavy load it reproduces thread starvation symptoms. Naming the monitor-versus-native distinction and the JEP 491 change shows current knowledge.
Context
This tests whether the candidate understands the scheduling model — virtual threads multiplex onto a fixed set of carriers, so CPU work is still bound by cores.
The mechanical answer
Virtual threads are scheduled onto a bounded set of carrier threads (by default a ForkJoinPool sized to available processors). A virtual thread running CPU-bound work occupies its carrier for the whole computation — it has nothing to yield on — so the total CPU throughput is the same as with platform threads. The benefit is purely about concurrency with blocking: virtual threads only win when the code spends time parked on I/O, allowing other virtual threads to reuse the carrier. CPU-bound workloads should still use parallelism tuned to cores (e.g., the number of virtual threads ≈ processors, or plain platform threads).
Trap
The trap answer is 'create more virtual threads for CPU work and it scales'. More virtual threads on CPU work just adds context-switch and continuation-mounting overhead on the same carriers. Another trap: using virtual threads for tasks that do heavy synchronized computation and assuming parallelism — contended monitors serialize them. The sharp answer ties the benefit to I/O-bound workloads and parking, which is exactly where async frameworks also win.