What specific code constructs cause a virtual thread to pin, and which common blocking operations do NOT?
answer
- Only two causes: synchronized + JNI/native
- Blocking is fine if the VT can unmount
- Socket I/O, sleep, ReentrantLock, BlockingQueue → no pin
- Harm = blocking WHILE pinned
- JEP 491 removes synchronized pinning; JNI remains
basics
~10 sPinning is caused by blocking inside a synchronized block/method (on older JDKs) and inside native/JNI calls. Ordinary blocking like socket I/O, Thread.sleep, BlockingQueue, and ReentrantLock do NOT pin—the virtual thread unmounts normally.
solid answer
~40 sThere are only two real pinning causes. First, blocking while holding a monitor—i.e., inside a `synchronized` block or method—because (pre-JDK 24/JEP 491) the monitor is bound to the carrier OS thread. Second, blocking inside a native method frame (a JNI call into C code), because the JVM can't unmount across native frames. Crucially, most 'blocking' you write does NOT pin: Loom reimplemented the JDK's blocking operations to unmount. So socket and file-channel I/O, `Thread.sleep`, `java.util.concurrent` locks like `ReentrantLock`, `BlockingQueue`, `CompletableFuture.get`, and `Future.get` all unmount cleanly. The trap is therefore narrow: it's specifically `synchronized` that *also blocks inside*, and native code. A non-blocking `synchronized` section (a quick increment) pins only for nanoseconds and is fine.
go deeper
Can name synchronized and native/JNI as the two pinning causes and knows that ordinary blocking (sleep, I/O) is safe.
Explains why I/O and j.u.c locks were reimplemented to unmount, and recognizes that the trap is specifically blocking inside synchronized or native frames.
Can audit a real codebase/library for hidden synchronized-around-blocking and native drivers, and reasons about which JDK version changes the answer for synchronized.
Sets dependency-vetting policy (which drivers/libraries are virtual-thread-safe), tracks JDK-version-dependent behavior across the fleet, and weighs FFM/JNI native boundaries in architecture decisions.
## The mental model: blocking is fine, *un-unmountable* blocking is not Virtual threads are designed so that when they block, they **unmount** from their carrier OS thread and free it. Pinning is the narrow set of cases where unmounting is impossible. So the question 'does X cause pinning?' reduces to 'can the JVM unmount the virtual thread while X blocks?' ## The two causes of pinning **1. Blocking inside a `synchronized` block or method (monitor pinning).** A `synchronized` region acquires an object's *monitor* (an intrinsic lock). The monitor's ownership is recorded against the underlying OS thread. To unmount the virtual thread you'd have to move that ownership to a different carrier later, which the pre-JDK-24 runtime can't do. So while a virtual thread sits inside `synchronized` and then blocks (e.g., waits, does I/O, calls a slow method), it stays pinned to its carrier. Note two subtleties: - The harm is *blocking while pinned*. A `synchronized` block that just mutates a field and returns pins for nanoseconds—negligible. - **JDK 24 / JEP 491** reworked the runtime so virtual threads *can* unmount inside `synchronized`. On JDK 24+ this cause is essentially gone. The advice below applies primarily to **JDK 21–23**. **2. Blocking inside a native method frame (JNI).** The **Java Native Interface (JNI)** lets Java call functions written in C/C++. When execution is down in native code, the Java stack is interleaved with native frames the JVM can't relocate, so it cannot unmount. If that native call blocks (e.g., a native database driver doing blocking I/O), the virtual thread pins. This cause remains even on JDK 24+. (A `Foreign Function & Memory` / FFM downcall has similar constraints.) ## What does NOT pin (the reassuring part) Project Loom went through the JDK and reimplemented blocking points to be unmount-aware. These all unmount cleanly and do **not** pin: - **Network I/O**: `Socket`, `SocketChannel`, `java.net.http.HttpClient`. - **`Thread.sleep`, `LockSupport.park`, `Object.wait`** (wait still needs the monitor but is handled). - **`java.util.concurrent` synchronizers**: `ReentrantLock`, `ReentrantReadWriteLock`, `Semaphore`, `CountDownLatch`, `CyclicBarrier`, `BlockingQueue` operations. - **`Future.get`, `CompletableFuture.get`, `ExecutorService.invokeAll`**. **One historical caveat:** some *file* I/O (`java.io.FileInputStream`, certain `FileChannel` paths) was implemented over native/blocking calls and could pin or block a carrier in early versions; behavior improved across releases, and `synchronized` use *inside* JDK internals (e.g., older `java.util.zip`, some buffered streams) was also a hidden source before those internals were de-`synchronized`. ## Putting it together When auditing code for pinning you are essentially looking for: (a) `synchronized` that wraps a blocking call, and (b) native/JNI libraries that block. Everything else is safe. This is why the canonical advice is so specific: 'replace blocking `synchronized` with `ReentrantLock`,' not 'avoid blocking.'
- If I have a synchronized method that does no blocking—just updates a counter—is that a pinning problem?No. It pins only for the nanoseconds it executes; there's no blocking while pinned, so the carrier is freed immediately. Pinning is a problem only when a thread blocks while pinned.
- Why might a third-party JDBC driver still cause pinning on JDK 24?JEP 491 fixed synchronized pinning but not native pinning. If the driver does blocking I/O through a JNI native call, the virtual thread can't unmount across the native frame and pins. The driver may also use synchronized internally on older JDKs.
saying these in an interview costs you the question
- Listing socket I/O or Thread.sleep as pinning causes—they unmount cleanly
- Saying ReentrantLock pins (it's the recommended non-pinning lock)
- Forgetting JNI/native as a cause and naming only synchronized
- Claiming a brief non-blocking synchronized increment is a pinning problem