🧵 Section 9 · Question #5

How does synchronized work at monitor level

At the monitor level, the synchronized keyword enforces mutual exclusion and memory visibility, guaranteeing that only one thread at a time can execute the critical section boun...


🟢 Junior Level

30-Second Summary

At the monitor level, the synchronized keyword enforces mutual exclusion and memory visibility, guaranteeing that only one thread at a time can execute the critical section bound to the target object’s monitor.

Its operational mechanics operate across two distinct architectural layers:

  1. Bytecode Level:
    • synchronized blocks compile into explicit monitorenter and monitorexit instructions, paired with a synthetic Exception table to ensure lock release even upon uncaught exceptions.
    • synchronized methods do not emit monitorenter/monitorexit bytecodes; instead, they set the ACC_SYNCHRONIZED access flag in the class method table, delegating lock management to JVM stack frame invocation and unwinding.
  2. JVM Level (HotSpot): Upon entry, the thread attempts an atomic CAS update on the object header’s Mark Word. If uncontended, it acquires a Lightweight Lock without kernel calls. If contended, it spins adaptively and then inflates to a native Heavyweight Lock (ObjectMonitor), parking the thread via OS system calls into the BLOCKED state.

Target Lock Object Resolution

Every synchronized construct is explicitly or implicitly bound to a specific object monitor:

Syntax Usage Target Monitor Object Bytecode Mechanism
public synchronized void method() Current instance (this) ACC_SYNCHRONIZED flag
public static synchronized void method() Class metadata (ClassName.class) ACC_SYNCHRONIZED flag
synchronized(lock) { ... } Explicit object reference (lock) monitorenter / monitorexit
synchronized(this) { ... } Current instance (this) monitorenter / monitorexit

Java 21 Code Examples

public class SynchronizedDemo {
    private final Object customLock = new Object();
    private int counter = 0;

    // 1. Instance method: Synchronized on 'this' via ACC_SYNCHRONIZED
    public synchronized void incrementInstance() {
        counter++;
    }

    // 2. Synchronized block: Explicit lock via monitorenter / monitorexit
    public void incrementBlock() {
        synchronized (customLock) {
            counter++;
        }
    }

    // 3. Static method: Synchronized on SynchronizedDemo.class
    public static synchronized void resetGlobal() {
        // Access static resources
    }
}

🟡 Middle Level

Bytecode Mechanics: Block vs. Method

Let us inspect the exact bytecode emitted by javac using javap -c -v:

1. Synchronized Block (monitorenter / monitorexit)

public void execute(Object lock) {
    synchronized (lock) {
        doSomething();
    }
}
public void execute(java.lang.Object);
  Code:
     0: aload_1              // Load 'lock' reference onto operand stack
     1: dup
     2: astore_2             // Store duplicate reference in local slot 2
     3: monitorenter         // ──► ATOMICALLY ACQUIRE MONITOR LOCK
     4: aload_0
     5: invokevirtual #7     // Method doSomething:()V
     8: aload_2              // Reload 'lock' reference
     9: monitorexit          // ──► RELEASE MONITOR LOCK (Normal Path)
    10: goto          18
    13: astore_3             // Exception handler entry (catches any Throwable)
    14: aload_2              // Reload 'lock' reference
    15: monitorexit          // ──► GUARANTEED RELEASE MONITOR LOCK (Exceptional Path)
    16: aload_3
    17: athrow               // Re-throw captured exception
    18: return
  Exception table:
     from    to  target type
         4    10    13   any   // Covers instructions inside critical section
        13    16    13   any   // Covers exception handler itself

2. Synchronized Method (ACC_SYNCHRONIZED)

public synchronized void executeMethod() {
    doSomething();
}
public synchronized void executeMethod();
  flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
     0: aload_0
     1: invokevirtual #7     // Method doSomething:()V
     4: return

Notice that no monitorenter or monitorexit instructions exist in the method bytecode. The JVM runtime checks ACC_SYNCHRONIZED during invocation:

  • For instance methods, it acquires the monitor of the instance passed in local variable 0 (this).
  • For static methods, it acquires the monitor of the corresponding java.lang.Class instance.
  • Monitor release is automatically handled by the JVM frame unwinding routine on normal return or abnormal exception propagation.

Reentrancy (Recursive Locking) Mechanics

Java monitors are strictly reentrant: if a thread already owns a monitor, it can re-enter any other code block guarded by the same monitor without deadlocking itself.

public synchronized void outer() {
    inner(); // Same monitor 'this', no deadlock occurs
}

public synchronized void inner() {
    // Critical execution
}

How HotSpot Implements Reentrancy:

  1. Lightweight Locking (Thin Lock):
    • The first acquisition pushes a BasicObjectLock (Lock Record) on the thread stack and CAS-stores a pointer into the object’s Mark Word.
    • Each nested re-entrant acquisition creates an additional Lock Record on the thread stack, but stores null as its Displaced Mark Word to indicate nesting.
    • Each monitorexit pops the top Lock Record. Only when the final Lock Record containing the non-null Displaced Mark Word is popped does the JVM execute the CAS to release the lock.
  2. Heavyweight Locking (Fat Lock):
    • In the C++ ObjectMonitor structure, the _recursions counter increments on each nested entry.
    • On monitorexit, _recursions decrements. The lock is only released to other threads when _recursions == 0.

🔴 Senior Level

Lock Escalation Pipeline in Modern HotSpot (Java 21)

To achieve near-zero overhead under uncontended conditions, HotSpot uses a multi-tier lock escalation pipeline:

[ Unlocked Object ] (Mark Word: 01)
         │
         │ Thread executes monitorenter
         v
[ Lightweight Locking (Thin Lock) ] (Mark Word: 00)
    • Stack Lock Record allocated
    • Atomic CAS (`lock cmpxchg`) writes Lock Record pointer to Mark Word
    • Execution cost: ~2–5 ns (Zero OS syscalls)
         │
         │ Contention detected (CAS fails)
         v
[ Adaptive Spinning ]
    • Contending thread spins in CPU loop executing PAUSE instructions
    • Avoids OS context switch if lock releases quickly
         │
         │ Spin budget exhausted
         v
[ Lock Inflation -> Heavyweight Lock ] (Mark Word: 10)
    • Native C++ ObjectMonitor allocated in native memory
    • Contending threads placed into _cxq / _EntryList
    • Thread descheduled via OS kernel syscall (Linux futex / Windows WaitForSingleObject)
    • Thread State: BLOCKED. Context switch penalty: ~1,000–5,000 ns

[!IMPORTANT] Biased Locking was deprecated in Java 15 and completely removed in Java 21 (JEP 416). In modern Java runtimes, uncontended synchronization begins directly at Lightweight Locking via stack CAS.


Java Memory Model (JMM) Barriers & Happens-Before

The JSR-133 specification establishes formal memory barrier requirements for monitors:

  1. monitorenter (Acquire Semantics):
    • Prevents memory reads and writes inside the critical section from being reordered before the monitor acquisition.
    • Flushes thread-local registers and invalidates CPU cache lines, forcing variable reads to load fresh values from coherent memory.
  2. monitorexit (Release Semantics):
    • Prevents memory writes inside the critical section from being delayed past the monitor release.
    • Drains CPU Store Buffers and commits all modified state to shared memory before the lock is relinquished.
  3. Happens-Before Relationship:

    An unlock on an intrinsic monitor (monitorexit) happens-before every subsequent lock (monitorenter) on the same monitor.


HotSpot Internal Queues: _cxq vs _EntryList vs _succ

Inside the ObjectMonitor structure, contending threads are organized using a two-tier queue hierarchy:

Arriving Contending Threads
           │
           │ Atomic CAS push (Lock-Free LIFO Stack)
           v
      [  _cxq  ] (Contention Queue)
           │
           │ Exiting owner moves nodes on release (Knob_QMode)
           v
     [ _EntryList ] (Ready List for next acquisition)
           │
           │ Designate successor thread
           v
       [ _succ ] (Successor Thread unparked)
  • _cxq (Contention Queue): A lock-free LIFO stack. Threads failing the CAS lock attempt push themselves atomically onto _cxq without acquiring any central lock.
  • _EntryList: A doubly-linked list. When the current owner releases the lock, it drains threads from _cxq into _EntryList.
  • _succ (Successor Optimization): To avoid the “Thundering Herd Problem” (waking all waiting threads simultaneously), the owner thread designates and unparks a single successor (_succ), which races to acquire the monitor.

JIT Compiler Optimizations: Lock Elision & Lock Coarsening

The HotSpot C2 compiler eliminates synchronization overhead using advanced static analysis:

  1. Lock Elision (Escape Analysis): If Escape Analysis confirms an object has NoEscape status (confined strictly to the thread/method stack), the compiler strips all monitorenter and monitorexit instructions from the machine code:
    public String formatKey(String key) {
        // StringBuffer is internally synchronized, but 'sb' never escapes
        StringBuffer sb = new StringBuffer();
        sb.append("KEY_").append(key);
        return sb.toString(); // JIT elides all lock operations completely!
    }
    
  2. Lock Coarsening: When multiple consecutive synchronized blocks operate on the same monitor without intervening un-synchronizable operations, the JIT compiler merges them into a single continuous synchronized block, eliminating repeated lock/unlock transitions.

4 Tricky Questions

1. What is the fundamental difference in exception handling between a synchronized block and a synchronized method?

Answer:

  • In a synchronized block, the javac compiler emits an explicit Exception table entry covering the instructions within the critical section. It generates an exceptional path with a duplicated monitorexit instruction followed by athrow.
  • In a synchronized method, no Exception table is generated for the monitor. Lock acquisition and release are handled implicitly by the JVM execution engine via the ACC_SYNCHRONIZED flag. If an unhandled exception bubbles up, the JVM runtime unbinds the monitor during stack frame unwinding before returning to the caller.

2. Why is synchronizing on boxed primitives (e.g., Integer, Long) or String literals considered a dangerous anti-pattern?

Answer:

  1. String Interning: String literals reside in the JVM String Pool and are shared across different classes and libraries. Synchronizing on "lock" can unintentionally cause independent subsystems to block each other or deadlock.
  2. Primitive Caching: Values from Integer.valueOf(-128) to Integer.valueOf(127) are cached by the JVM Integer Cache. Locking on Integer.valueOf(1) locks on a globally shared singleton instance.
  3. Valhalla Value-Based Classes: Boxed primitives are annotated with @ValueBased. In modern Java, synchronizing on them produces compiler warnings, and in future Java releases, it will throw IdentityException. Locks should always be dedicated instances: private final Object lock = new Object().

3. What happens if a thread is interrupted (Thread.interrupt()) while waiting in the BLOCKED state to enter a synchronized block?

Answer: The thread will NOT throw InterruptedException and will NOT abort its lock acquisition.

Waiting to enter an intrinsic monitor (monitorenter) is strictly uninterruptible. The thread remains queued in _cxq/_EntryList in the BLOCKED state until the monitor becomes available. The thread’s interrupt status flag is set to true, but the thread can only observe and handle it after it has successfully acquired the monitor and entered the critical section. If interruptible lock acquisition is required, Lock.lockInterruptibly() from java.util.concurrent.locks.ReentrantLock must be used.


4. Why does synchronized(this) almost never undergo JIT Lock Elision optimization?

Answer: Lock Elision requires the JIT compiler to prove via Escape Analysis that the locked object has NoEscape status (never escapes the allocating method).

The reference this represents an object that was already allocated outside the method and passed into it as local variable 0. Because external threads may hold references to this (ArgEscape or GlobalEscape), the compiler cannot guarantee that another thread will not attempt concurrent access. Therefore, the JIT must preserve all synchronization instructions on this. Lock Elision is practically restricted to locally instantiated objects.


🎯 Interview Cheat Sheet

Bytecode & Mechanism Matrix

| Feature | Synchronized Block | Synchronized Method | | :— | :— | :— | | Bytecode Instruction | monitorenter / monitorexit | None (Flag ACC_SYNCHRONIZED) | | Exception Handling | Synthetic Exception table with monitorexit | Handled natively by JVM stack frame unwinding | | Granularity | Fine-grained (arbitrary statement blocks) | Coarse-grained (entire method body) | | Target Object | Any explicit object reference | Implicit (this or ClassName.class) | | Lock Scope | Isolated dedicated locks possible | Exposes instance to external locking |


Key Takeaways

  • Dual Layers: Bytecode mechanics (monitorenter/monitorexit vs ACC_SYNCHRONIZED) work in tandem with JVM HotSpot Mark Word manipulation.
  • Escalation Path (Java 21): Lightweight Lock (00) $\to$ Adaptive Spinning $\to$ Heavyweight Lock (10 via ObjectMonitor). Biased locking is dead.
  • JMM Boundaries: monitorenter provides Acquire semantics; monitorexit provides Release semantics.
  • Uninterruptible: Waiting in BLOCKED for monitor entry ignores thread interruption signals.

Red Flags & Anti-Patterns to Avoid

  • ❌ Synchronizing on boxed primitives or String literals: Causes global lock contention and deadlocks.
  • ❌ Assuming synchronized methods are faster because of less bytecode: Modern JIT compilers yield identical execution performance; blocks provide superior architectural encapsulation.
  • ❌ Believing Thread.sleep() releases monitor locks: Only Object.wait() releases monitor locks; Thread.sleep() retains all held locks.
  • ❌ Exposing public lock objects: Always use private final Object lock = new Object() to prevent external classes from hijacking your monitor.