Java Virtual Threads: Understanding JDK 21 Limitations Before the JDK 25 LTS Release
Understanding Virtual Threads
Java Virtual Threads were introduced as a preview in JDK 19 and became stable in JDK 21. They promised to handle highly concurrent applications with thousands of lightweight tasks.
But first, let’s understand the problem they solved.
Traditional Java threads are expensive. Each thread maps directly to an OS thread, making them resource-heavy and limiting scalability.
Virtual threads changed this. They’re lightweight threads managed by the JVM, not the OS. You can create millions of them. Here’s how they work:
- Virtual threads run on top of carrier threads (platform threads)
- When a virtual thread blocks (waiting for I/O), it gets “unmounted” from its carrier
- The carrier thread is free to run other virtual threads
- When the blocking operation completes, any available carrier can “mount” the virtual thread again
This design makes virtual threads perfect for I/O-heavy applications — exactly what most web applications are.
However, JDK 21 had a significant limitation: pinning. Virtual threads could get stuck to their carrier threads during certain operations, defeating their main benefit.
The Synchronized Keyword: A Quick Refresher
The synchronized keyword has been Java's go-to for thread safety since day one. It does two things:
- Mutual exclusion: Only one thread can execute the synchronized code at a time
- Memory visibility: Changes made by one thread are visible to others
// Method-level synchronization
public synchronized void updateCounter() {
counter++;
}// Block-level synchronization
public void processData() {
synchronized(lockObject) {
// Critical section
sharedResource.update();
}
}Simple, reliable, and widely used in legacy codebases. But this is where virtual threads has major issue.
The Pinning Problem in Java 21
In Java 21, virtual threads had a major limitation. When a virtual thread entered a synchronized block and performed a blocking operation, it got "pinned" to its platform thread.
When Pinning Leads to Deadlocks
The pinning problem wasn’t just about performance, it could cause complete application deadlocks. Here’s how:
- Virtual threads enter synchronized blocks and get pinned to carrier threads
- They make blocking calls (database, I/O) while still pinned
- All available carrier threads become occupied with pinned virtual threads
- New virtual threads can’t get carrier threads to run on
- The app freezes, a typical deadlock.
Here’s what happened:
synchronized(lock) {
// Database call or I/O operation
String data = database.fetch(); // This caused pinning in Java 21
return processData(data);
}When pinned, the virtual thread couldn’t release its underlying platform thread, even while waiting. This defeated the whole purpose of virtual threads, being lightweight and allowing one platform thread to serve many virtual threads.
What Causes Pinning?
The Java 24 Improvment
Java 24 fixed the biggest pinning issue: synchronized blocks no longer pin virtual threads.
Now when a virtual thread blocks inside a synchronized block, it can release its carrier thread. Any available platform thread can later resume the work when the blocking operation completes.
Real Performance Impact
Here’s what this pinning problem actually cost in real applications.
Performance Test: 5,000 Virtual Threads with Database Calls In my benchmark, each thread acquired a lock, did some processing, then made a simulated database call (5ms delay) inside a synchronized block:
- Java 21: 31–32 seconds total
- Java 24: 0.463 seconds total
Why such a massive difference? In Java 21, each virtual thread stayed pinned to its carrier thread during the database wait. With limited carrier threads (typically matching CPU cores), most virtual threads had to queue up, waiting for a carrier to become available.
In Java 24, virtual threads release their carriers during the database call, letting those carriers serve other threads immediately.
This represents a 98% performance improvement just from upgrading Java versions. So it is clear: Java 21’s pinning problem made virtual threads unsuitable for applications using synchronized blocks with I/O operations. Java 24 resolves this issue entirely.
Why This Matters
If you’re building applications that:
- Handle high concurrent loads
- Use legacy code with
synchronizedblocks - Make database calls or external service calls
- Need better resource utilization
Then Java virtual threads in 24+ are a game-changer. You get the scalability benefits without rewriting your synchronized code.
Looking Ahead: Java 25 LTS (September 2025)
With Java 25 LTS releasing in just less than a month (September 16, 2025), this is the perfect time to understand virtual threads’ evolution. Java 25 will offer:
- Long-term support stability for enterprise adoption
- Mature virtual thread implementation with Java 24’s pinning fixes included
- Production-ready confidence for mission-critical applications
If you’re planning your next LTS upgrade, understanding this virtual thread journey from Java 21’s limitations to Java 24’s breakthrough helps you make informed decisions.
Bottom Line
Java 21 introduced virtual threads but had the pinning problem. Java 24 solved it. Java 25 LTS delivers the mature version.
If you tried virtual threads in Java 21 and hit performance issue, Java 24+ is your answer. If you haven’t tried them yet, Java 25 LTS is the right time to start.
The pinning problem is resolved. Virtual threads are now ready for high-throughput production applications with no more choosing between legacy code compatibility and performance.
