Java synchronized Lock Contention: CAS, Adaptive Spinning, and Three-Tier Upgrade

This article explains how the JVM handles threads that fail to acquire a synchronized lock, detailing the three-tier upgrade from lightweight CAS-based spinning to adaptive ObjectMonitor spinning and finally OS-level blocking, with Java 17's simplified lock states and Mark Word transitions.

samdeepthink
samdeepthink
samdeepthink
Java synchronized Lock Contention: CAS, Adaptive Spinning, and Three-Tier Upgrade

Interview Question: What Happens When a Thread Cannot Acquire a synchronized Lock?

The article opens with a classic Java interview scenario: two threads, A and B, attempt to enter a synchronized block. Thread A acquires the lock; what happens to thread B? The candidate suggests the OS suspends the thread. The interviewer challenges this: if the critical section is very short (e.g., incrementing a variable, taking dozens of nanoseconds), suspending and later resuming the thread incurs two context switches costing microseconds — far longer than the lock hold time. The candidate then proposes spinning (busy-waiting). But if the lock is held for a long time, spinning wastes CPU cycles. This dilemma leads to the core topic: Compare-And-Swap (CAS) and the JVM's lock upgrade mechanism.

Cost of Direct Suspension

Suspending a thread involves saving CPU registers, flushing pipelines, and other context-switch overhead, typically in the microsecond range. If the critical section completes in tens of nanoseconds, the suspension/resumption cost dwarfs the actual wait time. Early JVM versions did suspend immediately; later versions introduced optimizations to avoid this.

Spinning and Its Drawback

Spinning keeps the thread on the CPU, repeatedly attempting to acquire the lock. If the lock is released quickly, a few dozen CPU cycles suffice, avoiding a context switch. However, if the lock holder runs for a long time, spinning becomes pure CPU waste. The JVM therefore adopts a hybrid strategy: try spinning first, then fall back to suspension. But spinning requires a low-cost way to retry lock acquisition; if each retry required a system call (kernel transition), spinning would not be worthwhile.

CAS: The Low-Cost Lock Acquisition Mechanism

CAS (Compare-And-Swap) is a CPU atomic instruction that reads a memory location, compares it with an expected value, and if equal, writes a new value — all without interruption. The article demonstrates CAS with Java 17 code using AtomicInteger:

AtomicInteger lock = new AtomicInteger(0);
// Expect 0 (unlocked), try to set to 1 (locked)
boolean first = lock.compareAndSet(0, 1);
// Second attempt: current value is 1, expected 0 mismatches, fails
boolean second = lock.compareAndSet(0, 1);

Output on Windows 11:

first CAS: true
second CAS: false
current value: 1

The first CAS succeeds because the value matches the expected 0; the second fails because the value is now 1. The JVM's synchronized locking uses a similar approach.

How Lightweight Locking Works

For synchronized(obj), the lock object's Mark Word (part of the object header) holds state. In the unlocked state (lowest two bits 01), the thread uses CAS to replace the Mark Word with a pointer to its own stack-allocated BasicLock. On success, the Mark Word's lowest bits become 00, indicating a lightweight lock is held.

If CAS fails, another thread holds the lock. In JIT-compiled code, the CAS is wrapped in a short retry loop — this is the lightweight lock's spin. The retry count is not fixed; it depends on the compiler and CPU architecture. The entire process stays in user mode, avoiding kernel transitions. If the lock is released quickly, a few CPU cycles acquire it, much faster than a context switch. But if the holder does not release promptly, the CAS retries exhaust, triggering lock inflation.

Inflation to Heavyweight Lock After Spin Failure

When lightweight spinning fails, the JVM creates an ObjectMonitor and inflates the lock to heavyweight. The Mark Word changes from 00 to 10, now pointing to the ObjectMonitor instead of a BasicLock.

Thread B enters the ObjectMonitor and does not suspend immediately. The monitor lets it spin again — but this time the spin duration is adaptive . Each ObjectMonitor maintains a spin counter adjusted by historical success rate: successful spins increase the counter (more spinning next time), failures decrease it (less spinning or skip spinning). This adaptive spinning avoids both premature suspension and excessive CPU burn.

Only when adaptive spinning ultimately fails does the thread suspend, entering the ObjectMonitor 's wait queue and yielding the CPU.

Three-Step Upgrade Path

Fast Path : CAS attempt to acquire lock, no kernel entry, lowest cost. On failure, inflate to heavyweight.

ObjectMonitor Adaptive Spin : Dynamically adjusted spin duration based on past success; checks if lock holder is runnable. Medium cost.

Blocking/Suspension : Thread suspends, yields CPU. Highest cost, involves context switch.

Each step's cost increases; the JVM escalates only when the lower-cost step proves ineffective.

Java 17 Simplifies Lock States

In Java 8, synchronized had four states: unlocked, biased, lightweight, heavyweight. Biased locking assumed most locks are accessed by a single thread repeatedly, storing the thread ID in the Mark Word to avoid CAS on re-entry. This assumption breaks down in multi-core, high-concurrency scenarios. Biased lock revocation requires a global safepoint synchronization, whose overhead outweighs benefits. Java 15 deprecated biased locking.

Java 17 reduces states to three: unlocked → lightweight → heavyweight. Upgrade is one-way; no downgrade from heavyweight to lightweight.

Lightweight vs. Heavyweight Lock Comparison

Mark Word lock bits : lightweight 00, heavyweight 10.

Lock acquisition : lightweight uses CAS on object header; heavyweight uses ObjectMonitor mutex.

On contention : lightweight spins in CPU loop; heavyweight suspends thread.

CPU consumption : lightweight consumes CPU during spin; heavyweight consumes none after suspension.

Context switch : lightweight has none; heavyweight incurs microsecond-level overhead.

Suitable scenarios : lightweight for short critical sections, low contention; heavyweight for long critical sections, high contention.

Summary

The JVM's synchronized implementation exemplifies a common systems design pattern: layering to handle different cost magnitudes. Lightweight locks use CAS for user-mode acquisition at minimal cost. ObjectMonitor 's adaptive spin forms a middle layer, dynamically tuning based on history. OS-level suspension, the costliest, is the last resort. This gradual escalation — try cheapest first, upgrade only when necessary — appears in many systems: TCP retransmission backoff, database query optimization (index before full scan), and more.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JVMconcurrencyCASSynchronizedJava 17heavyweight locklightweight lockadaptive spinning
samdeepthink
Written by

samdeepthink

Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.