backenddrills

Java 21 concurrency

Debugging Java 21 virtual-thread pinning

By the BackendDrills editorial team · Published and checked October 6, 2026 · 6-minute read

Before you start: Executor basics and resource budgets. Find this reading in a study path →

In Java 21, a virtual thread can remain pinned to its carrier while blocking inside a synchronized region. Frequent, long blocking episodes can limit scalability. Diagnose with recording evidence and the deployed JDK version; a large virtual-thread count alone does not prove pinning.

Start with the version and the resource

Suppose a statement-export service uses a virtual thread per request. Load testing shows rising latency even though computation is modest. All exports pass through a synchronized helper that waits for a remote archive response. Record the exact runtime version, deployment concurrency limits and downstream budgets before changing the synchronization primitive.

// Illustrative Java 21 fragment.
synchronized (archiveGate) {
    return archiveClient.readStatement(statementId); // blocking I/O
}

Two problems can overlap here: carrier pinning and serialization behind the shared gate. Removing pinning would not by itself let multiple requests enter a critical section that is intentionally exclusive. First state what the lock protects and whether the remote wait must occur under that protection.

Collect a recording that can challenge the diagnosis

Java 21 Flight Recorder supports jdk.VirtualThreadPinned events. Correlate event duration and stacks with the latency interval and the suspected operation. Inspect a thread dump and dependency timings too. Absence of recorded events is meaningful only after checking recording settings and the duration threshold.

# Read an existing recording from a controlled test.
jfr print --events jdk.VirtualThreadPinned recording.jfr
ObservationInterpretation to test
Pinned events align with the archive waitBlocking under a Java 21 monitor contributes
Long wait to enter the shared gateThe critical section serializes exports
Archive time rises with request concurrencyThe dependency may be saturated
Many threads wait for database connectionsThe database pool remains a separate limit

Change the narrowest relevant boundary

Move blocking work outside the monitor if a correct design allows it. If mutual exclusion around the operation is required on Java 21, evaluate a suitable lock with try/finally cleanup, while preserving the guarded invariant. Also bound in-flight archive requests: virtual threads make waiting inexpensive in some respects, but do not create unlimited remote capacity.

Java 24 changed synchronized-monitor pinning behavior. Do not apply the Java 21 diagnosis unchanged to a newer runtime, and do not assume an upgrade removes lock contention, native-call constraints or downstream saturation. Reproduce on the version you intend to deploy.

Set a recovery gate

Run the same workload before and after the change. Compare completed valid exports, p95 latency, relevant recording events and archive errors. Include cancellation while waiting and concurrent requests for the same statement. A useful gate is a measurable latency budget with no corrupted or duplicated export, under a documented arrival rate.

Check the underlying behavior

Original illustrative examples, prepared with AI assistance and checked against the linked primary documentation. No customer incident or vendor endorsement is claimed. Our editorial approach.

Practice the next decision

Try a complete free backend drill: inspect evidence, make three decisions and review the reasoning. No account or card required.

Try a free incident drill →

Explore 1008 scenarios · $28 one-time

Selected practice from the study paths

Free readings need no account. Full edition drills require verified access; opening a paid link does not expose its answers.

Recommended next readings