skip to content

Pinning Pitfalls

A pinned virtual thread cannot unmount and holds its carrier hostage, which happens inside synchronized blocks and native frames. Detecting it with jdk.tracePinnedThreads and replacing synchronized with ReentrantLock is the answer interviewers expect.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What specific code constructs cause a virtual thread to pin, and which common blocking operations do NOT?

level: middleimportance: must knowfreq 60%

answer

  1. Only two causes: synchronized + JNI/native
  2. Blocking is fine if the VT can unmount
  3. Socket I/O, sleep, ReentrantLock, BlockingQueue → no pin
  4. Harm = blocking WHILE pinned
  5. JEP 491 removes synchronized pinning; JNI remains

basics

~10 s

Pinning 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 s

There 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

for a junior

Can name synchronized and native/JNI as the two pinning causes and knows that ordinary blocking (sleep, I/O) is safe.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

What is virtual-thread pinning in Java, and why does it matter?

level: middleimportance: must knowfreq 70%

basics

~20 s

Pinning is when a virtual thread can't detach from its OS carrier thread while it blocks, so it keeps that carrier busy. That wastes a scarce carrier and can stall other virtual threads, hurting throughput.

open as a page

How do you mitigate virtual-thread pinning, and why does replacing synchronized with ReentrantLock work?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Replace blocking synchronized blocks/methods with java.util.concurrent.locks.ReentrantLock using lock()/unlock() in a try/finally. ReentrantLock lets the virtual thread unmount while it waits, so the carrier stays free. Also keep critical sections short and avoid blocking inside them.

open as a page

How do you detect virtual-thread pinning in a running Java application?

level: seniorimportance: should knowfreq 48%

basics

~10 s

Run with -Djdk.tracePinnedThreads to print a stack trace whenever a thread blocks while pinned (older JDKs), or record a JFR profile and look for jdk.VirtualThreadPinned events. Both show exactly where the pinning happens.

open as a page

How did JEP 491 change synchronized pinning, and what should still concern an architect adopting virtual threads at scale?

level: principalimportance: should knowfreq 35%

basics

~20 s

JEP 491 (JDK 24) reworked the runtime so a virtual thread can unmount even while holding a synchronized monitor, so synchronized no longer pins. Native/JNI blocking can still pin, and you must still watch carrier-pool sizing and thread-locals at scale.

open as a page