What happens when a virtual thread executes a blocking call such as Thread.sleep or blocking I/O?
answer
- blocking call -> unmount from carrier -> carrier freed
- JDK blocking ops are VT-aware (I/O, sleep, locks, BlockingQueue)
- continuation parked on heap; remount on any carrier to resume
- OS never the one waiting -> sync code, async scalability
- pinning = blocked but stays mounted (synchronized/JNI)
basics
~20 sThe virtual thread pauses and unmounts from its carrier (the platform thread running it), freeing that carrier to run other virtual threads. When the blocking call is ready to continue, the virtual thread is remounted and resumes. The carrier is never wasted just waiting.
solid answer
~50 sJDK blocking operations are virtual-thread-aware. When a virtual thread hits a blocking call — blocking socket/file I/O via the reworked java.io and NIO, Thread.sleep, java.util.concurrent locks, BlockingQueue, Future.get, etc. — the JVM captures (parks) its continuation and unmounts it from its carrier platform thread. The carrier is then free to mount and run other ready virtual threads, so a few carriers can serve millions of mostly-blocked virtual threads. Under the hood, blocking I/O registers interest with the OS (epoll/kqueue) and the carrier moves on; when data arrives the virtual thread becomes runnable and is remounted onto some carrier to continue exactly where it left off. The crucial point: the OS thread is never the thing left waiting, so you write straightforward synchronous blocking code and still get the scalability of asynchronous I/O. This is why virtual threads shine for I/O-bound workloads.
go deeper
Knows that a blocking call pauses the virtual thread and frees the underlying platform thread to do other work.
Explains unmount/remount onto carriers, lists which JDK blocking calls are VT-aware, and knows it's for I/O-bound (not CPU-bound) work.
Describes the continuation/parking mechanism and OS-event integration, and explains pinning (synchronized/JNI) plus the ReentrantLock mitigation.
Reasons about carrier-pool sizing, pinning impact on tail latency under load, observability of mount/unmount, and when async/reactive still beats virtual threads.
## The problem virtual threads solve Blocking code is easy to read: `var data = socket.read(); process(data);`. But on a **platform thread** (a thread backed 1:1 by an OS thread), a blocking call means the underlying OS thread sits idle in the kernel, doing nothing, until the I/O completes. OS threads are scarce (a few thousand max), so a server that holds one per in-flight request can't scale to many concurrent slow requests. The historical workaround was *asynchronous, non-blocking* code (callbacks, `CompletableFuture`, reactive streams) — scalable but hard to read and debug. Virtual threads give you the easy blocking style *and* the scalability. ## Mounting, unmounting, carriers, continuations - **Carrier thread**: a real platform (OS) thread from a small JVM-managed pool (a `ForkJoinPool` by default). It is the *engine* that actually executes code. - **Mount**: a virtual thread runs by being mounted onto a carrier — its stack frames are placed on the carrier and the carrier executes them. - **Continuation**: the saved state (stack) of a virtual thread that lets it be paused and later resumed exactly where it stopped. - **Unmount / park**: when the virtual thread can't make progress (it blocks), the JVM saves its continuation onto the heap and detaches it from the carrier. The carrier is now free to mount a *different* runnable virtual thread. ## What happens on a blocking call, step by step 1. The virtual thread calls a **JDK blocking operation** — these were specifically reworked to cooperate: blocking socket/file I/O (`java.net`, `java.nio` channels, the rebuilt `java.io` streams), `Thread.sleep`, `java.util.concurrent` locks (`ReentrantLock`, etc.), `BlockingQueue.take/put`, `Future.get`, `CompletableFuture.join`, `Object.wait`, latches, semaphores. 2. The JDK does not block the carrier. For I/O it registers interest with the OS event mechanism (epoll on Linux, kqueue on macOS, IOCP on Windows) and **unmounts** the virtual thread. 3. The carrier is returned to the scheduler and immediately picks up another ready virtual thread. **No OS thread is left idle.** 4. When the I/O completes (or the sleep elapses, or the lock is acquired), the virtual thread is marked runnable. The scheduler **remounts** it onto some available carrier — not necessarily the same one — and it resumes right after the blocking call as if nothing happened. Net effect: a handful of carriers (typically equal to CPU core count) can keep millions of virtual threads in flight, because at any instant most are unmounted, waiting. ## When it does NOT unmount cleanly: pinning A virtual thread can get **pinned** — it blocks but stays mounted, tying up its carrier (which defeats the purpose). The classic causes are: blocking **inside a `synchronized` block/method** (the monitor was tied to the carrier), and calling into **native code (JNI)**. While pinned, that carrier can't serve other virtual threads. Mitigations: prefer `java.util.concurrent.locks.ReentrantLock` over `synchronized` around blocking calls. (In later JDKs much `synchronized` pinning was eliminated, but JNI/native frames still pin — and the principle remains an interview staple.) ## Why this matters - You write **simple synchronous blocking code** and get **async-grade scalability**. - Virtual threads help **I/O-bound** workloads (lots of waiting). For **CPU-bound** work they give no scalability win — you're limited by cores, and unmounting never happens because the thread never blocks. - A `Thread.sleep` on a virtual thread is cheap (it just unmounts); the same sleep on a platform thread wastes a scarce OS thread.
- What is pinning and how do you avoid it?Pinning is when a virtual thread blocks but stays mounted on its carrier, so the carrier can't run other virtual threads. The classic causes are blocking inside a synchronized block/method and calling native (JNI) code. Avoid it by replacing synchronized with ReentrantLock around blocking sections.
- Do virtual threads make CPU-bound work faster?No. They scale concurrency for blocking/waiting (I/O-bound) work. CPU-bound work is bounded by core count, and a busy thread never blocks, so it never unmounts to free a carrier.
saying these in an interview costs you the question
- Saying the carrier thread blocks/waits during virtual-thread I/O
- Claiming virtual threads speed up CPU-bound work
- Forgetting that synchronized blocks (or JNI) can pin the carrier
- Thinking the thread always remounts onto the same carrier