Virtual Threads in Java 21 — Project Loom Explained
Fast recall
Quick revision
Core answer
ForkJoinPool sized to CPUs). When a virtual thread hits a blocking I/O call (socket read, HTTP, JDBC), the JVM unmounts it from the carrier — stack gets parked on the heap, carrier goes to serve another virtual thread. The memory cost is kilobytes, not megabytes — a platform thread's 1 MB default stack becomes a resizable heap object. Create them with Thread.ofVirtual().start(task) or Executors.newVirtualThreadPerTaskExecutor(). The goal: kill thread pools for I/O-bound server code and let you write straight-line blocking logic without callback hell or reactive complexity.Whether the candidate can articulate what virtual threads change (cheap blocking I/O), how they're scheduled (carrier threads, unmount on block), and when they apply vs don't (I/O-bound yes, CPU-bound no)
Thinking virtual threads make CPU-bound code faster (they don't — still limited by CPUs). Or assuming you should now use them everywhere, including synchronized-heavy code where pinning causes carrier exhaustion
Explain the mount/unmount model concretely, mention pinning on synchronized and native frames, note that ThreadLocal is 'fine but heavy' for millions of threads (ScopedValue alternative), and that the whole point is eliminating the need for reactive frameworks for typical request-handler code
Interview-ready guide
Interview answer
Virtual threads are Java 21's Project Loom feature that lets you write simple blocking code that scales to millions of concurrent threads.
The problem they solve
- Traditional platform threads cost ~1 MB of stack each; you run out around 5K-10K
- For high concurrency, teams were forced into async/reactive frameworks (Netty, Reactor, WebFlux)
- Async comes with callback hell,
CompletableFuturechains, broken ThreadLocals, unreadable stack traces - Virtual threads let you go back to one-thread-per-request blocking code — but scale like async
The mechanism
- Virtual threads are subclasses of
java.lang.Thread— same API - Scheduled by the JVM onto a pool of carrier threads (platform threads sized to your CPUs)
- When a virtual thread hits blocking I/O, the JVM unmounts it:
- Stack copied to the heap
- Carrier freed to run another virtual thread
- I/O completion reschedules the virtual thread back onto any carrier
- All blocking operations in the JDK (sockets, HttpClient, JDBC) are instrumented
- Transparent to the code you write
Numbers
- Memory: from ~1 MB per platform thread down to ~few KB per virtual thread
- Count: thousands → millions on typical hardware
- Parallelism unchanged = CPU count
- Concurrency limit = heap memory, not thread count
How to create
Thread.ofVirtual().start(task)— builderExecutors.newVirtualThreadPerTaskExecutor()— idiomatic executor formThread.startVirtualThread(runnable)— one-liner- Never pool them — creation is microseconds
What doesn't change
- CPU-bound work doesn't get faster — still capped at CPU count
- Don't use virtual threads for tight loops or heavy computation
Known limitations
- Pinning:
synchronizedblocks and native frames prevent unmounting. Blocking I/O inside synchronized holds a carrier. At scale this causes carrier starvation. Fix: migrate toReentrantLock - ThreadLocal: works fine functionally but millions of threads each carrying their own copy is expensive. Use
ScopedValue(Java 21 preview) for immutable per-scope values - Downstream saturation: a million virtual threads can easily overwhelm a DB previously protected by a small thread pool. Use Semaphore for flow control
Good fits
- HTTP servers, REST APIs, JDBC-heavy backends
- Message consumers, outbound fan-out calls
- Migration target from reactive code that was async-for-blocking-I/O
Bad fits
- CPU-bound work (use ForkJoinPool)
- Heavy synchronized code blocking on I/O (refactor first)
- Working WebFlux/Netty systems (don't rewrite for the sake of it)
Recommendation: For any new server-side Java project dealing with I/O-bound workloads — REST APIs, message handlers, outbound HTTP fan-out — start with `Executors.newVirtualThreadPerTaskExecutor()` and simple blocking code. You get reactive-level scaling without reactive complexity. Audit existing code for `synchronized` blocks that wrap blocking I/O and migrate those to ReentrantLock before switching — otherwise pinning will bite you under load. Keep a `Semaphore`-based admission control for downstream protection, since the implicit rate limiting of small thread pools no longer exists. For CPU-bound code, keep using traditional thread pools. Virtual threads aren't a silver bullet — they're the right tool specifically for high-concurrency blocking I/O, and they're now the default choice there.
Virtual Threads in Java 21 — Project Loom Explained
Direct Answer
Virtual threads (JEP 444, Java 21 GA) are lightweight, JVM-managed threads that let you write simple one-thread-per-request blocking code while scaling to millions of concurrent threads. They look and feel identical to java.lang.Thread (they are subclasses) but are scheduled by the JVM onto a small pool of carrier threads (by default, ForkJoinPool sized to CPUs). When a virtual thread hits a blocking I/O call (socket read, HTTP, JDBC), the JVM unmounts it from the carrier — stack gets parked on the heap, carrier goes to serve another virtual thread. The memory cost is kilobytes, not megabytes — a platform thread's 1 MB default stack becomes a resizable heap object. Create them with Thread.ofVirtual().start(task) or Executors.newVirtualThreadPerTaskExecutor(). The goal: kill thread pools for I/O-bound server code and let you write straight-line blocking logic without callback hell or reactive complexity.
Common Mistakes & Pitfalls
Thinking virtual threads make CPU-bound code faster (they don't — still limited by CPUs). Or assuming you should now use them everywhere, including synchronized-heavy code where pinning causes carrier exhaustion
Check your understanding
Answer these without looking back at the article.
- How does a virtual thread actually yield when it hits blocking I/O?
- Why do `synchronized` blocks cause pinning, and what's the workaround?
- Should you pool virtual threads? Why or why not?
- What's `ScopedValue` and when would you use it instead of `ThreadLocal`?
- If virtual threads are unlimited, how do you prevent overloading a downstream service?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.