Understanding Virtual Thread Pinning: Causes, Detection, and Solutions

From Stripgay, the free encyclopedia of technology

Introduction

Virtual threads enable Java applications to scale efficiently for I/O-intensive workloads without the complexity of managing platform threads. Unlike traditional platform threads, which are expensive OS resources demanding careful handling and non-blocking I/O patterns (e.g., CompletableFuture), virtual threads are lightweight and managed by the JVM scheduler. However, even with this abstraction, certain scenarios can cause a virtual thread to block its underlying carrier thread—a phenomenon known as pinning. While pinning does not break business logic, it hinders scalability by preventing the carrier thread from executing other virtual threads. This article explores common pinning scenarios, demonstrates how to detect them using Java Flight Recorder (JFR), and discusses mitigation strategies, including improvements in JDK 24.

Understanding Virtual Thread Pinning: Causes, Detection, and Solutions
Source: www.baeldung.com

What Causes Virtual Thread Pinning?

Pinning occurs when a virtual thread gets mounted onto a platform (carrier) thread and then blocks for an extended period, preventing the carrier from unmounting and servicing other virtual threads. The JVM scheduler normally unmounts a virtual thread when it performs blocking operations like I/O or Thread.sleep(), but certain constructs force the virtual thread to stay attached. The primary causes include:

  • Synchronized blocks or methods (using synchronized keyword)
  • Native method calls that block internally
  • CPU-intensive operations (though these should be avoided on virtual threads anyway)

Among these, the most frequent source is the use of synchronized because it relies on operating system mutexes, which cannot be released without the carrier thread.

The Synchronized Block Scenario

Consider a shopping cart service where updates to a product's quantity are protected by an object-level lock. The following CartService class 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();
            products.merge(productId, quantity, Integer::sum);
        }
        LOGGER.info("Updated Cart for {} {}", productId, quantity);
    }

    private void simulateAPI() {
        try {
            Thread.sleep(50);
        } catch (InterruptedException ex) {
            throw new RuntimeException(ex);
        }
    }
}

Here, simulateAPI() mimics a downstream API call by sleeping for 50 milliseconds. When a virtual thread enters the synchronized block, it acquires the intrinsic lock and cannot be unmounted by the JVM scheduler—even during Thread.sleep(). This causes the carrier thread to be pinned for the duration of the block, reducing concurrency.

Detecting Pinning with Java Flight Recorder

Java Flight Recorder (JFR) provides an event type jdk.VirtualThreadPinned that records pinning occurrences. To demonstrate, we can write a test that runs the CartService.update() inside a virtual thread while JFR is enabled:

@Test
void givenJFRIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws Exception {
    Path file = Path.of("pinning.jfr");
    try (Recording recording = new Recording()) {
        recording.enable("jdk.VirtualThreadPinned");
        recording.start();
        // Execute update inside a virtual thread
        Thread.startVirtualThread(() -> cartService.update("product-1", 2));
        Thread.sleep(200); // Wait for completion
        recording.stop();
        recording.dump(file);
        // Parse JFR events to verify pinning
        assertTrue(containsPinnedEvent(file));
    }
}

After running this test, examining the JFR file will reveal a VirtualThreadPinned event. The event includes stack traces showing the synchronized block as the culprit. You can analyze these events using tools like JDK Mission Control or programmatically via the jdk.jfr.consumer API.

Understanding Virtual Thread Pinning: Causes, Detection, and Solutions
Source: www.baeldung.com

Mitigation Strategies

To prevent pinning, avoid using synchronized blocks in code executed by virtual threads. Instead, leverage java.util.concurrent.locks.ReentrantLock or other high-level concurrency utilities that allow the JVM to unmount the virtual thread while waiting for a lock. The revised CartService might look like:

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();
        }
        LOGGER.info("Updated Cart for {} {}", productId, quantity);
    }
}

With ReentrantLock, the virtual thread can be unmounted while waiting for the lock, and the carrier thread remains free. Other strategies include restructuring code to avoid long-held locks or using immutable data structures. For CPU-intensive tasks, it's advisable to run them on a dedicated platform thread pool rather than virtual threads.

Improvements in JDK 24

The JDK team continues to refine virtual thread behavior. In JDK 24, some previously unresolved pinning scenarios are addressed. Specifically, the JVM can now unmount virtual threads during Thread.sleep() inside a synchronized block under certain conditions, reducing the impact. However, full elimination of pinning for all synchronized use cases remains a work in progress. Developers are encouraged to follow JEP 425 and subsequent enhancements.

For more details on virtual threads and best practices, refer to the introduction and causes section.