Symptoms & Diagnosis: Java 21 Virtual Thread Starvation
Java 21 introduced virtual threads to revolutionize concurrency, but many developers are encountering unexpected “starvation” issues. This occurs when the underlying carrier threads (ForkJoinPool) become saturated, preventing new virtual threads from being scheduled.
The primary symptom is a sudden spike in latency despite low CPU usage. Unlike platform threads, virtual threads depend on “parking.” If a virtual thread pins its carrier thread—usually due to a synchronized block or native method—the carrier thread cannot switch to other tasks.
| Symptom | Potential Diagnostic Meaning |
|---|---|
| Increased Response Latency | Virtual threads are waiting for a free carrier thread. |
| Low CPU / High Memory Usage | Threads are blocked but not being released from the carrier. |
| Carrier Thread Exhaustion | All ForkJoinPool worker threads are “pinned” by synchronized blocks. |
Identifying this requires looking for “pinning” events. When a virtual thread is pinned, it effectively turns into a heavy platform thread, negating the benefits of Project Loom.

Troubleshooting Guide: Identifying the Bottleneck
To fix Java 21 slow performance, you must first verify if pinning is occurring. The JVM provides built-in system properties to trace these events. You can enable specific flags to log every instance where a thread fails to unmount from its carrier.
Run your application with the following system property to output stack traces whenever a virtual thread pins its carrier thread:
java -Djdk.tracePinnedThreads=full -jar your-application.jar
Analyze the logs for synchronized keywords. If the stack trace shows a thread holding a monitor while performing a blocking I/O operation (like database calls or network requests), you have found the source of your starvation.
Another powerful tool is the JDK Flight Recorder (JFR). You can monitor jdk.VirtualThreadPinned events to visualize how often and for how long threads are staying pinned. High durations here are a direct cause of “Slow Performance” in Java 21 environments.
Prevention: Best Practices for Java 21 Performance
Fixing starvation is not about increasing thread pools; it is about changing how code handles locks. The most effective prevention strategy is migrating away from intrinsic locks.
1. Replace Synchronized with ReentrantLock
In Java 21, ReentrantLock allows virtual threads to unmount from the carrier thread when they encounter a lock. Unlike synchronized, which pins the thread, ReentrantLock is “virtual-thread friendly.”
2. Avoid Blocking I/O inside Synchronized Blocks
If you cannot remove a synchronized block, ensure that no blocking operations (network, file I/O, or heavy computation) happen inside it. Keep synchronized sections as short as possible to minimize the window of pinning.
3. Limit the Use of ThreadLocals
Excessive use of ThreadLocal can lead to memory overhead when dealing with millions of virtual threads. Use ScopedValues where possible to maintain high performance and prevent memory-related starvation.
By following these steps, you can eliminate the virtual thread starvation fix bottleneck and achieve the high-throughput performance Java 21 promised.