<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Build With Haseeb]]></title><description><![CDATA[Build With Haseeb]]></description><link>https://haseebansari.blog</link><image><url>https://cdn.hashnode.com/uploads/logos/6253f757b32ddd968bbe8eb7/b74b4da5-4da6-4fa3-86ef-55e939bf3dca.png</url><title>Build With Haseeb</title><link>https://haseebansari.blog</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 11 May 2026 18:06:17 GMT</lastBuildDate><atom:link href="https://haseebansari.blog/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why the heck Java 21 Virtual Threads Get Pinned]]></title><description><![CDATA[Introduction
Java 21 introduced virtual threads as a new concurrency model designed to simplify asynchronous programming. Instead of carefully managing thread pools and non-blocking APIs, developers c]]></description><link>https://haseebansari.blog/why-the-heck-java-21-virtual-threads-get-pinned</link><guid isPermaLink="true">https://haseebansari.blog/why-the-heck-java-21-virtual-threads-get-pinned</guid><category><![CDATA[Java]]></category><category><![CDATA[virtualthreads]]></category><category><![CDATA[concurrency]]></category><category><![CDATA[scalability]]></category><category><![CDATA[software development]]></category><category><![CDATA[jvm]]></category><dc:creator><![CDATA[Haseeb Ansari]]></dc:creator><pubDate>Fri, 27 Mar 2026 18:43:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6253f757b32ddd968bbe8eb7/d7a77cdb-dbc2-45f6-8e21-fb47999ff054.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>Java 21 introduced virtual threads as a new concurrency model designed to simplify asynchronous programming. Instead of carefully managing thread pools and non-blocking APIs, developers could write straightforward blocking code and still achieve high scalability.</p>
<p>In practice, this works well for many workloads. However, under certain conditions, virtual threads behave very differently than expected. A key issue is <strong>pinning</strong>, where a virtual thread becomes tied to its underlying platform thread, preventing efficient scheduling.</p>
<p>This article explains why pinning occurs in Java 21, how it affects performance, and what changes in Java 24 through <a href="https://openjdk.org/jeps/491"><strong>JEP 491</strong></a>.</p>
<h1>First, a refresher on Virtual Threads</h1>
<img src="https://cdn.hashnode.com/uploads/covers/6253f757b32ddd968bbe8eb7/b7c9bb96-db44-4698-b992-4d4b83ec86a3.png" alt="" style="display:block;margin:0 auto" />

<p>Virtual threads are scheduled by the JVM rather than the operating system. Multiple virtual threads are multiplexed onto a smaller set of platform threads, often referred to as carrier threads.</p>
<p>This design allows applications to handle a large number of concurrent tasks without requiring a proportional number of OS threads. The key property is that a virtual thread can be suspended during blocking operations, allowing the carrier thread to run other work.</p>
<p>This suspension and remounting behavior is what enables scalability.</p>
<h1>The Pinning Problem</h1>
<p>In Java 21, virtual threads can become pinned to their carrier threads. When this happens, the virtual thread cannot be unmounted, and the carrier thread remains occupied.</p>
<p>Pinning typically occurs when a virtual thread executes within a <code>synchronized</code> block or method and performs a blocking operation.</p>
<p>At a high level, this breaks the expected behavior of virtual threads. Instead of releasing the carrier thread when blocked, the virtual thread holds onto it, reducing overall concurrency.</p>
<h2>Why Pinning Happens</h2>
<p>The root cause lies in how <code>synchronized</code> is implemented.</p>
<p>A <code>synchronized</code> block relies on a monitor lock to enforce mutual exclusion. These monitors are managed by the JVM at the platform thread level. When a thread enters a synchronized block, it acquires the monitor, and other threads must wait until the lock is released.</p>
<p>With platform threads, this model works as expected. With virtual threads, the interaction is more subtle.</p>
<p>When a virtual thread enters a synchronized block, the monitor is effectively associated with its carrier thread. If the virtual thread blocks while holding the monitor, the carrier thread is also forced to wait. The JVM cannot safely detach the virtual thread from the carrier in this state.</p>
<p>As a result, the virtual thread remains mounted, and the carrier thread cannot be reused.</p>
<h2>A Concrete Example</h2>
<p>Consider the following method:</p>
<pre><code class="language-java">public synchronized String getData(Socket socket) throws IOException {         
    return new String(socket.getInputStream().readAllBytes()); 
}
</code></pre>
<p>If a virtual thread executes this method and the socket does not immediately provide data, the call to <code>readAllBytes()</code> blocks.</p>
<p>At this point:</p>
<ul>
<li><p>The virtual thread is waiting on I/O</p>
</li>
<li><p>The monitor lock is still held</p>
</li>
<li><p>The carrier thread is also blocked</p>
</li>
</ul>
<p>Because the lock is tied to the carrier thread, the JVM cannot unmount the virtual thread. The carrier thread is effectively pinned until the operation completes and the lock is released.</p>
<h2>Impact on Scalability</h2>
<p>Pinning reduces the effectiveness of virtual threads in two ways.</p>
<p>First, it prevents carrier threads from being reused. Each pinned virtual thread occupies a platform thread for the duration of the blocking operation.</p>
<p>Second, it limits concurrency under contention. If multiple virtual threads compete for the same monitor and perform blocking operations, the system begins to resemble traditional thread-per-request models.</p>
<p>This is particularly problematic in I/O-heavy systems, where blocking is common and virtual threads are expected to provide the most benefit.</p>
<h2>Workarounds in Java 21</h2>
<p>To avoid pinning, developers often replaced <code>synchronized</code> with alternatives such as <code>ReentrantLock</code> or redesigned critical sections to avoid blocking operations while holding locks.</p>
<p>These approaches can reduce the likelihood of pinning, but they introduce additional complexity and move away from the simplicity that virtual threads were meant to provide.</p>
<h1>The Java 24 Fix: JEP 491</h1>
<p><a href="https://openjdk.org/jeps/491"><strong>JEP 491</strong></a> addresses this limitation by changing how virtual threads interact with monitor locks.</p>
<p>In Java 24, virtual threads are no longer pinned when contending for or holding monitors in the same way as before. The JVM allows virtual threads to be unmounted even when synchronization is involved, ensuring that carrier threads can continue executing other tasks.</p>
<p>This change preserves the semantics of <code>synchronized</code> while restoring the scalability benefits of virtual threads.</p>
<h2>What This Means in Practice</h2>
<p>With the improvements in Java 24:</p>
<ul>
<li><p>Blocking inside synchronized code no longer necessarily ties up a carrier thread</p>
</li>
<li><p>Virtual threads can be suspended and resumed more freely</p>
</li>
<li><p>The scheduler can maintain higher throughput under contention</p>
</li>
</ul>
<p>In effect, developers can use <code>synchronized</code> without undermining the core advantages of virtual threads.</p>
<h2>Conclusion</h2>
<p>Virtual threads simplify concurrent programming, but their behavior depends on subtle interactions within the JVM.</p>
<p>In Java 21, pinning exposed a limitation in how synchronization and scheduling worked together. Under specific conditions, virtual threads could lose their lightweight nature and behave more like traditional threads.</p>
<p>Java 24 resolves this with JEP 491, making virtual threads more predictable and scalable in real-world scenarios.</p>
<p>Understanding these details is important when adopting new concurrency models. The abstraction is simple, but the underlying behavior still matters especially at scale.</p>
]]></content:encoded></item></channel></rss>