Java 21 ยท Project Loom
Spring Boot 3 ยท Java 21 ยท Verified Fix

Virtual Thread Pinning in Spring Boot 3.2+

How synchronized blocks and ThreadLocal variables silently cripple high-throughput Project Loom applications โ€” and how to eliminate them.

Framework
Spring Boot 3.x
Runtime
Java 21 LTS
Stability
Enterprise Grade

Technical Briefing

Virtual threads (Project Loom) are designed to be lightweight and massively scalable, supporting millions of concurrent tasks on a handful of OS threads. However, 'pinning' occurs when a virtual thread becomes stuck to its carrier (platform) thread โ€” preventing the JVM scheduler from mounting other virtual threads onto it. This commonly happens inside synchronized blocks that perform blocking I/O, or when invoking native methods. The result is silent performance degradation: no exception thrown, just thread exhaustion.

โš  Signal Detected

exception_report.log
FATAL
// Thread Dump via jcmd <pid> Thread.dump_to_file -format=json
"VirtualThread-1" #25 virtual
   java.lang.VirtualThread.park(VirtualThread.java:582)
   java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)
   ...
   at com.example.service.UserService.findUser(UserService.java:42)
   - pinned on <0x00000007abc12345> (a java.lang.Object)

// JVM Output when using: -Djdk.tracePinnedThreads=full
Thread[ForkJoinPool-1-worker-1,5,CarrierThreads]
    com.example.service.UserService.findUser(UserService.java:42) <== monitors:1

โ—Ž Trace Analysis

The JVM 21 runtime cannot unmount a virtual thread from its carrier when the virtual thread enters a monitor (synchronized block or method). If I/O blocking occurs while holding that monitor โ€” such as a database call or REST request โ€” the entire OS carrier thread blocks. With a ForkJoinPool carrier pool of typically CPU_CORES threads, just a handful of pinned virtual threads can starve the entire scheduling pool.

โœฆ Remediation Plan

  1. Replace every synchronized block and method with java.util.concurrent.locks.ReentrantLock โ€” virtual threads can yield while waiting to acquire it.

  2. Remove long-lived ThreadLocal usage from async/virtual thread paths; replace with ScopedValue (Preview in Java 21, stable in Java 23).

  3. Upgrade JDBC drivers to Loom-aware versions: PostgreSQL 42.6+, MySQL Connector/J 8.3+, Oracle 23.3+.

  4. Add -Djdk.tracePinnedThreads=full in staging environments to identify all pinning sites before they hit production.

  5. Set spring.threads.virtual.enabled=true in Spring Boot 3.2+ and monitor carrier thread saturation with JFR or Micrometer.

Production Implementation
SafeJava 21
// โŒ PINNING TRAP โ€” synchronized blocks pin the carrier thread
class="hi-ann">@Service
public class UserService {
    private final Object lock = new Object();

    public User findUser(Long id) {
        synchronized (lock) {
            // repository.findById() blocks on JDBC I/O
            // The entire carrier thread is now pinned!
            return repository.findById(id).orElseThrow();
        }
    }
}

// โœ… LOOM-SAFE FIX โ€” ReentrantLock allows virtual threads to yield
class="hi-ann">@Service
public class UserService {
    private final ReentrantLock lock = new ReentrantLock();

    public User findUser(Long id) {
        lock.lock();  // Virtual thread parks here, carrier is FREE to run others
        try {
            return repository.findById(id).orElseThrow();
        } finally {
            lock.unlock();
        }
    }
}

// โœ… MODERN PATTERN โ€” ScopedValue instead of ThreadLocal
// application.yml
// spring:
//   threads:
//     virtual:
//       enabled: true  # Enable virtual threads globally in Boot 3.2+

โŸ Engineering Deep-Dive

How Carrier Thread Scheduling Works

Java 21's virtual thread scheduler is a work-stealing ForkJoinPool. By default it creates CPU_CORES carrier (platform) threads. When a virtual thread calls a blocking operation โ€” network I/O, file read, Thread.sleep() โ€” the JVM unmounts it from the carrier and parks it. The carrier is immediately free to pick up another virtual thread from the run queue.

This cooperative scheduling is what makes one million concurrent virtual threads feasible. A carrier does real CPU work continuously; it never blocks. Pinning breaks this contract.

Identifying Pinning in Production

Use -Djdk.tracePinnedThreads=full in staging to log every pinning event with a full stack trace. In production, use Java Flight Recorder (JFR) โ€” it has a built-in VirtualThreadPinned event with zero-allocation overhead. Alert on it in your observability stack.

Third-Party Library Risk

Many mature libraries โ€” connection pools, caching layers, messaging clients โ€” heavily use synchronized. HikariCP 5.1.0+ is Loom-aware. For others, audit with JFR before enabling virtual threads in production. A single pinning hot-path in a dependency can silently cap your throughput.

ScopedValue: The ThreadLocal Successor

ThreadLocal works with virtual threads but carries a memory cost: each virtual thread gets its own copy of the value, and with millions of virtual threads that can mean significant heap pressure. ScopedValue is immutable and structurally scoped to a call tree โ€” lower memory, safer semantics, and zero copy-on-inherit cost.

โ—‡ Elite Standards

  1. Engineering Rule

    Audit all third-party dependencies with JFR VirtualThreadPinned events before going live โ€” libraries are often the biggest source of hidden pinning.

  2. Engineering Rule

    Treat synchronized as a code smell in virtual-thread-heavy services; enforce a linting rule in your CI pipeline.

  3. Engineering Rule

    Keep carrier pool size as Runtime.getRuntime().availableProcessors() (the default). Increasing it masks pinning bugs rather than fixing them.

FAQ

What causes Virtual Thread Pinning in Spring Boot 3.2+ in Spring Boot 3?
The JVM 21 runtime cannot unmount a virtual thread from its carrier when the virtual thread enters a monitor (synchronized block or method). If I/O blocking occurs while holding that monitor โ€” such as a database call or REST request โ€” the entire OS carrier thread blocks. With a ForkJoinPool carrier pool of typically CPU_CORES threads, just a handful of pinned virtual threads can starve the entire scheduling pool.
How do I fix Virtual Thread Pinning in Spring Boot 3.2+?
Replace every synchronized block and method with java.util.concurrent.locks.ReentrantLock โ€” virtual threads can yield while waiting to acquire it. Remove long-lived ThreadLocal usage from async/virtual thread paths; replace with ScopedValue (Preview in Java 21, stable in Java 23). Upgrade JDBC drivers to Loom-aware versions: PostgreSQL 42.6+, MySQL Connector/J 8.3+, Oracle 23.3+. Add -Djdk.tracePinnedThreads=full in staging environments to identify all pinning sites before they hit production. Set spring.threads.virtual.enabled=true in Spring Boot 3.2+ and monitor carrier thread saturation with JFR or Micrometer.
Best practice #1 for preventing Java 21 ยท Project Loom errors?
Audit all third-party dependencies with JFR VirtualThreadPinned events before going live โ€” libraries are often the biggest source of hidden pinning.
Best practice #2 for preventing Java 21 ยท Project Loom errors?
Treat synchronized as a code smell in virtual-thread-heavy services; enforce a linting rule in your CI pipeline.
Best practice #3 for preventing Java 21 ยท Project Loom errors?
Keep carrier pool size as Runtime.getRuntime().availableProcessors() (the default). Increasing it masks pinning bugs rather than fixing them.

Feedback

Live
ML

M. Leachouri

Founder & Chief Architect

"I built Kodivio because professional tools shouldn't come at the cost of your privacy. Our mission is to provide enterprise-grade utilities that process data exclusively in your browser."

M. Leachouri is an Expert Web Developer, Data Scientist Engineer, and Systems Architect with a deep specialization in DevOps and Cybersecurity. With over a decade of experience building scalable distributed systems and Zero-Trust architectures, he engineered Kodivio to bridge the gap between high-performance computing and absolute user sovereignty.

Verified Expert
Certified Architect
Full Profile & Mission โ†’