JEP 491, Synchronize Virtual Threads without Pinning, has been promoted to Proposed to Target status for JDK 24. This proposal aims to overhaul how Java’s synchronized
methods interact with virtual threads to eliminate the longstanding issue of “pinning” that limits thread scalability. Virtual threads, introduced in JDK 21, enable increased throughput of applications utilizing hundreds of thousands of threads. However, pinning — a limitation due to synchronized
keyword — has restricted Java developers from fully capitalizing on this innovation. JEP 491 addresses this issue, paving the way for high-performance, high-concurrency applications without requiring extensive refactoring.
In Java, synchronization is achieved with monitors associated with each object. A thread entering a synchronized
block or method acquires a lock on the monitor. This ensures that only one thread can execute within that block or method at a time. However, in Java’s current model, the JVM registers the monitor lock with the platform thread — not the virtual thread. This association has resulted in pinning: once a virtual thread acquires a lock, it is bound to the underlying platform thread, preventing the scheduler from reallocating it to other virtual threads.
Consider the following example, where a method reads bytes from a socket:
synchronized byte[] getData() {
byte[] buf = ...;
int nread = socket.getInputStream().read(buf); // Can block here
...
}
If this method blocks during the read operation, the JVM will pin the virtual thread to its platform thread, essentially consuming the resources until data is available. Such blocking limits scalability, as pinned virtual threads occupy platform threads that could otherwise support additional virtual threads. JEP 491 proposes allowing virtual threads to unmount even within synchronized blocks, freeing their platform carriers to execute other virtual threads.
To help developers identify problematic code, the JDK currently records a jdk.VirtualThreadPinned
event in JDK Flight Recorder (JFR) whenever a virtual thread blocks within a synchronized method. This event has been invaluable for pinpointing areas where developers may need to refactor code or switch from synchronized
to java.util.concurrent
locks to avoid pinning. For instance, code that involves heavy I/O operations within synchronized blocks leads to pinning. However, with JEP 491’s proposed changes, the jdk.VirtualThreadPinned
event will only remain relevant for scenarios where virtual threads interact with native code.
Furthermore, the jdk.tracePinnedThreads
system property, which logs stack traces when virtual threads block, will no longer be necessary and will be removed to prevent performance issues.
This JEP proposes reimplementing the synchronized
keyword, which will enable virtual threads to acquire, hold, and release monitors without tying these actions to their carrier threads. The JVM scheduler will allow blocked virtual threads to unmount from platform threads, releasing those threads for other tasks. This will involve changes to the Object.wait()
and Object.notify()
mechanisms, allowing virtual threads to suspend and resume without locking their carrier platform threads.
For example, a virtual thread that calls Object.wait()
in a synchronized block will unmount and remount when signaled to continue, as shown below:
synchronized void waitForCondition() {
while (!condition) {
wait(); // Virtual thread unmounts here
}
...
}
With this approach, applications can scale better without requiring developers to abandon synchronized
in favour of java.util.concurrent
locks.
JEP 491 acknowledges that a few exceptional scenarios will continue to pin virtual threads. These cases include blocking inside class initializers, waiting for class initialization by other threads, and blocking while resolving symbolic references during class loading. While these instances are relatively rare, they may cause issues under specific conditions, particularly in highly concurrent applications. The proposal suggests monitoring these scenarios and revisiting them in future updates if they become problematic.
One alternative to JEP 491 is to increase the parallelism of the virtual thread scheduler to mitigate the effects of pinning. However, this approach is limited by a default cap of 256 platform threads to the scheduler. Another option involves rewriting bytecode to replace synchronized
with ReentrantLock
dynamically, but this solution would entail high overhead and complexity, which can potentially compromise performance and compatibility with existing JVM features.
As Java continues to evolve, JEP 491 will serve as a critical step toward refining virtual threads and simplifying Java’s concurrency model. The proposed changes will allow developers to build high-throughput applications without the trade-offs previously required.