How to Detect and Fix Virtual Thread Pinning in Java
Introduction
Virtual threads in Java 21+ revolutionize concurrent programming by allowing developers to create millions of lightweight threads without worrying about operating system limits. However, a common pitfall known as pinning can occur when a virtual thread holds a lock that prevents the carrier platform thread from being reused, reducing scalability. This guide walks you through identifying pinning scenarios, debugging them with Java Flight Recorder (JFR), and applying fixes—including using ReentrantLock and taking advantage of JDK 24 enhancements.

What You Need
- Java 21 or later (JDK 24 recommended for latest pinning fixes)
- Java Flight Recorder (JFR) enabled (usually default in JDK)
- An IDE or text editor (e.g., IntelliJ, VS Code)
- Maven or Gradle for building the project
- Basic understanding of Java concurrency and virtual threads
Step-by-Step Guide
Step 1: Understand Virtual Thread Pinning Scenarios
Virtual threads are mounted on platform (carrier) threads for execution. Pinning occurs when a virtual thread cannot be unmounted from its carrier thread, typically because of:
- Synchronized blocks or methods – The JVM must keep the virtual thread pinned to the carrier to respect intrinsic locks.
- Native method execution or CPU-intensive tasks – These block the carrier thread directly.
- Blocking I/O inside a critical section – Holding a lock while waiting for I/O prevents unmounting.
While heavy CPU work should be avoided in virtual threads altogether, locking issues are fixable. This guide focuses on the synchronized scenario.
Step 2: Set Up Sample Code with a Synchronized Block
Create a class CartService that simulates updating a shopping cart. The update method uses a synchronized block on a per-product lock:
public class CartService {
private final Map<String, Integer> products = new ConcurrentHashMap<>();
private final Map<String, Object> locks = new ConcurrentHashMap<>();
public void update(String productId, int quantity) {
Object lock = locks.computeIfAbsent(productId, k -> new Object());
synchronized (lock) {
simulateAPI(); // Simulates downstream API call
products.merge(productId, quantity, Integer::sum);
}
System.out.println("Updated Cart for " + productId + " " + quantity);
}
private void simulateAPI() {
try {
Thread.sleep(50); // Simulates blocking I/O
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
This code holds the synchronized lock while sleeping, which can cause pinning.
Step 3: Enable JFR and Run a Virtual Thread Task
Use JFR to capture events when a virtual thread is pinned. Create a test that starts a recording, executes the update method inside a virtual thread, and stops the recording:
import jdk.jfr.Recording;
import java.nio.file.Path;
@Test
void testPinningDetection() throws Exception {
Path file = Path.of("pinning.jfr");
try (Recording recording = new Recording()) {
recording.enable("jdk.VirtualThreadPinned"); // Enable pinning event
recording.start();
Thread.ofVirtual().start(() -> {
CartService cart = new CartService();
cart.update("product-1", 2);
}).join();
recording.stop();
recording.dump(file);
}
}
Run the test. The recording will be saved to pinning.jfr.
Step 4: Analyze JFR Events for Pinning
Open the pinning.jfr file in JDK Mission Control (or parse it programmatically). Look for VirtualThreadPinned events. They indicate the virtual thread was pinned on a platform thread during the synchronized block. Count and examine stack traces to confirm the pinned location (e.g., CartService.update).

If no events appear, pinning is absent; otherwise, proceed to fix.
Step 5: Replace Synchronized with ReentrantLock
The most straightforward fix is to replace synchronized with ReentrantLock, which does not cause pinning because the lock is managed at the Java level, not the JVM intrinsic lock level. Modify CartService:
import java.util.concurrent.locks.ReentrantLock;
public class CartService {
private final Map<String, Integer> products = new ConcurrentHashMap<>();
private final Map<String, ReentrantLock> locks = new ConcurrentHashMap<>();
public void update(String productId, int quantity) {
ReentrantLock lock = locks.computeIfAbsent(productId, k -> new ReentrantLock());
lock.lock();
try {
simulateAPI();
products.merge(productId, quantity, Integer::sum);
} finally {
lock.unlock();
}
System.out.println("Updated Cart for " + productId + " " + quantity);
}
// simulateAPI remains the same
}
Now the lock is explicitly acquired and released; virtual threads can yield while waiting for the lock, avoiding pinning.
Step 6: Re-run and Verify No Pinning
Run the same JFR test again with the modified code. After execution, open the new recording. You should see zero VirtualThreadPinned events. This confirms the fix works.
Step 7: Consider JDK 24 Improvements
JDK 24 introduces further reductions in pinning scenarios. For example, synchronized blocks will no longer cause pinning when the lock is uncontested (i.e., no contention) – the JVM can unmount the virtual thread before entering the block. However, for robust code, it's still best to avoid synchronized in virtual-thread-heavy code. Also note that native method calls and CPU-heavy work remain pinning-prone.
Tips for Avoiding Virtual Thread Pinning
- Prefer
java.util.concurrent.locks(e.g.,ReentrantLock,StampedLock) oversynchronizedblocks when using virtual threads. - Avoid blocking operations inside critical sections – keep locks held only for brief, non-blocking operations.
- Use
Thread.sleep(0)if you need a yield; it does not pin. - Monitor with JFR regularly to catch unexpected pinning early.
- Consider
java.util.concurrent.LinkedTransferQueuefor custom hand-offs that avoid locking. - Stay updated with JDK releases – each version may improve virtual thread behavior.
Related Articles
- 7 Key Insights into Kubernetes v1.36's Mutable Pod Resources for Suspended Jobs
- Nature's Built-In Armor: How Scorpions Fortify Their Weapons with Metals
- Mastering Markdown on GitHub: A Beginner's Q&A Guide
- How to Get Started with Microsoft's New Professional Certificates on Coursera
- Global Math Gender Gap Widens: Girls' Post-Pandemic Recovery Lags Behind Boys
- The Hidden Cost of Transforming Schools: A Black Educator's Story of Burnout and Resilience
- How a Self-Taught Coder Created an AI Agent to Crack Leaderboards
- New Step-by-Step Guide Empowers Go Developers to Containerize Apps with Docker