🧵 Section 9 · Question #4

What is monitor in Java

A Monitor in Java is a high-level synchronization and mutual exclusion construct, originally conceptualized by C.A.R. Hoare and Per Brinch Hansen and implemented natively in Jav...


🟢 Junior Level

30-Second Summary

A Monitor in Java is a high-level synchronization and mutual exclusion construct, originally conceptualized by C.A.R. Hoare and Per Brinch Hansen and implemented natively in Java at the language and JVM levels.

Every object in Java is endowed with an intrinsic monitor (intrinsic lock or monitor lock). The monitor serves two fundamental responsibilities:

  1. Mutual Exclusion (Mutex): Ensures that at any given moment, exactly one thread can execute a critical section of code guarded by that monitor (via the synchronized keyword).
  2. Cooperation and Signaling: Enables threads to coordinate work by relinquishing lock ownership and waiting for specific conditions to become true via wait(), notify(), and notifyAll().

Architectural Components of a Monitor

Conceptually, a Java monitor is composed of three distinct logical entities:

  • Lock Owner: A reference to the single active thread currently holding the monitor and executing inside the critical section.
  • Entry Set (EntryList / _cxq): The set of threads attempting to acquire the monitor when it is already held by another thread. These threads reside in the Thread.State.BLOCKED state.
  • Wait Set (WaitSet): The collection of threads that invoked wait() while holding the monitor. They voluntarily released the lock and are awaiting an explicit wake-up signal (notify() / notifyAll()). These threads reside in the Thread.State.WAITING or TIMED_WAITING state.
                    +------------------------------------+
                    |  Thread executes monitorenter      |
                    +-----------------+------------------+
                                      |
                                      v
                             [   Entry Set  ]  <--------------------+
                          (State: BLOCKED)                          |
                                      |                             | notify() /
                   Monitor is free    | Lock acquired               | notifyAll()
                                      v                             |
                             +-----------------+                    |
                             |  Owner Thread   |                    |
                             | (State: RUNNING)|                    |
                             +--------+--------+                    |
                                      |                             |
                      lock.wait()     |                             |
               (releases the monitor) v                             |
                             [   Wait Set   ] ----------------------+
                        (WAITING / TIMED_WAITING)

Java 21 Code Examples

public class BoundedBlockingQueue<T> {
    private final Object lock = new Object(); // Dedicated monitor lock object
    private final int capacity;
    private final java.util.Queue<T> queue = new java.util.LinkedList<>();

    public BoundedBlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public void put(T item) throws InterruptedException {
        synchronized (lock) {
            // Guard with a while loop to protect against Spurious Wakeups
            while (queue.size() == capacity) {
                lock.wait(); // Releases lock and transitions thread to WaitSet (WAITING)
            }
            queue.add(item);
            lock.notifyAll(); // Wakes up threads waiting in WaitSet, moving them to EntrySet
        } // Monitor is released upon exiting the synchronized block
    }

    public T take() throws InterruptedException {
        synchronized (lock) {
            while (queue.isEmpty()) {
                lock.wait();
            }
            T item = queue.poll();
            lock.notifyAll();
            return item;
        }
    }
}

🟡 Middle Level

Internal Architecture: Object Header and Mark Word

In the HotSpot JVM, every Java object allocated on the heap contains an Object Header, which consists of:

  1. Klass Word: A pointer to the class metadata in Metaspace (4 bytes with compressed class pointers enabled, otherwise 8 bytes).
  2. Mark Word: A 64-bit native machine word reflecting object runtime state: identity hash code, garbage collection generational age, and locking state.

Mark Word Bit States (HotSpot 64-bit, Java 21)

The lowest two bits of the Mark Word determine the monitor lock status:

Lock State Bias Bit State Bits (Tag) Mark Word Content Description
Unlocked 0 01 Hashcode (31 bits), GC Age (4 bits), unused bits
Lightweight Locked (Thin Lock) 0 00 Direct pointer to Lock Record on thread stack
Heavyweight Locked (Fat Lock) 0 10 Direct pointer to native C++ ObjectMonitor structure
Marked for GC 0 11 Reserved header used by GC marking phases

[!NOTE] Biased Locking was deprecated in Java 15 (JEP 374) and completely removed in Java 21 (JEP 416). In Java 21, lock escalation begins immediately with Lightweight Locking without checking or revoking biased thread IDs.


Anatomy of C++ ObjectMonitor in HotSpot

When lock contention causes a monitor to inflate into a Heavyweight Lock, the JVM allocates a native C++ ObjectMonitor structure (src/hotspot/share/runtime/objectMonitor.hpp):

class ObjectMonitor {
    void*             _header;       // Displaced mark word of the Java object
    void*             _object;       // Pointer back to the Java heap object
    Thread* volatile  _owner;        // Pointer to current owning thread
    volatile intx     _recursions;   // Reentrant nesting depth counter
    
    // Contending thread queues:
    ObjectWaiter* volatile _cxq;       // Contention Queue (LIFO lock-free stack)
    ObjectWaiter* volatile _EntryList; // Ready queue of threads competing for ownership
    ObjectWaiter* volatile _WaitSet;   // Doubly linked circular list of threads in wait()
    
    volatile intx     _WaitSetLock;  // Spinlock protecting the WaitSet structure
    Thread* volatile  _succ;         // Designated successor thread
};

Queue Roles: _cxq vs _EntryList

  • _cxq (Contention Queue): A lock-free LIFO singly-linked stack where newly arriving threads are atomically inserted via CAS instructions when the monitor is locked.
  • _EntryList: A queue populated by the lock owner upon monitor release. The exiting thread transfers threads from _cxq into _EntryList and signals the successor thread _succ.

Bytecode Mechanics: monitorenter and monitorexit

Synchronized blocks compile into explicit pairs of bytecode instructions:

public void execute(Object lock) {
    synchronized (lock) {
        doWork();
    }
}

Bytecode disassembly via javap -c:

public void execute(java.lang.Object);
  Code:
     0: aload_1              // Push 'lock' reference to operand stack
     1: dup
     2: astore_2             // Store reference in local variable 2
     3: monitorenter         // Acquire intrinsic monitor
     4: aload_0
     5: invokevirtual #7     // Method doWork:()V
     8: aload_2
     9: monitorexit          // Normal exit path: release monitor
    10: goto          18
    13: astore_3             // Exception handler entry: catch any Throwable
    14: aload_2
    15: monitorexit          // Exceptional exit path: guaranteed monitor release!
    16: aload_3
    17: athrow
    18: return
  Exception table:
     from    to  target type
         4    10    13   any
        13    16    13   any

[!IMPORTANT] The javac compiler generates an internal Exception table covering the critical section with target type any. If an unhandled Throwable or Error occurs, control routes to offset 13, executing monitorexit before re-throwing the exception. This guarantees intrinsic locks never remain permanently locked due to exceptions.


🔴 Senior Level

Lock Escalation Pipeline in Java 21

In modern HotSpot runtimes, monitor locks transition along a unidirectional escalation pipeline:

[ Unlocked Object (01) ]
       │
       │ Thread enters synchronized block
       v
[ Lightweight Lock (00) ]  <--- Lock Record on thread stack (CAS on Mark Word)
       │
       │ Contention encountered (CAS fails) + Adaptive spinning exhausted
       v
[ Heavyweight Lock (10) ]  <--- ObjectMonitor allocated, thread parked via OS (futex / mutex)

1. Lightweight Locking (Thin Lock)

  • Upon entering synchronized, a BasicObjectLock (Lock Record) is allocated on the thread’s execution stack.
  • The object’s original Mark Word is saved into the Lock Record (termed the Displaced Mark Word).
  • The JVM issues a hardware atomic instruction (lock cmpxchg) attempting to write the stack pointer to the Lock Record into the object’s Mark Word.
  • If successful, the thread owns the thin lock without invoking operating system primitives, executing in just a few CPU cycles.
  • Reentrant acquisitions push another Lock Record containing a null Displaced Mark Word, serving as a recursion nesting marker.

2. Inflation to Heavyweight Lock (Fat Lock)

If another thread attempts to acquire the monitor while it is held:

  1. The CAS fails.
  2. The contending thread executes Adaptive Spinning, spinning in a tight loop across CPU PAUSE instructions, betting that the owner will finish shortly.
  3. If spinning expires without acquiring the lock, Lock Inflation triggers:
    • The JVM allocates a native C++ ObjectMonitor structure.
    • The Displaced Mark Word is moved into ObjectMonitor._header.
    • The object’s Mark Word is updated via CAS to point to the ObjectMonitor with state bits set to 10.
    • The contending thread adds itself to _cxq and parks via an OS kernel syscall (futex on Linux or WaitForSingleObject on Windows), transitioning to Thread.State.BLOCKED.

Asynchronous Monitor Deflation (Java 15/17+)

Historically, inflated ObjectMonitor structures could only be deflated back to the Unlocked state during Stop-The-World (STW) garbage collection safepoints, adding significant GC pause overhead.

Since Java 15+ (JEP 350 / JEP 374), HotSpot uses Asynchronous Monitor Deflation:

  • A dedicated background JVM thread (MonitorDeflationThread) scans idle ObjectMonitor instances in a safepoint-free manner.
  • If _owner == null, _cxq == null, _EntryList == null, and _WaitSet == null, the JVM restores the Displaced Mark Word back to the object header and frees the native ObjectMonitor memory without pausing application threads.

The Impact of Object.hashCode() on Monitor Inflation

A subtle edge-case in HotSpot internals involves System.identityHashCode(obj):

  • In an Unlocked object, the 31-bit identity hashcode is cached directly inside the Mark Word.
  • When an object is Lightweight Locked, its Mark Word is overwritten by a pointer to the thread’s stack Lock Record.
  • If hashCode() or System.identityHashCode() is invoked on an object held under a Lightweight Lock, the JVM cannot store the 31-bit hash code on the thread stack without causing data loss when the lock is released.
  • Direct Consequence: Calling hashCode() on a locked object immediately forces lock inflation to a Heavyweight ObjectMonitor, because only ObjectMonitor._header can permanently preserve both the identity hashcode and the lock state simultaneously.

JIT Compiler Optimizations

The HotSpot C2 compiler optimizes synchronized code aggressively:

  1. Lock Elision (Escape Analysis): If Escape Analysis confirms that a synchronized target object never escapes the current method or thread stack, the compiler completely eliminates monitorenter and monitorexit from the compiled assembly:
    public String buildLog(String msg) {
        // StringBuffer is synchronized, but 'sb' does not escape
        StringBuffer sb = new StringBuffer();
        sb.append("LOG: ").append(msg);
        return sb.toString(); // JIT elides all lock operations completely
    }
    
  2. Lock Coarsening: When the compiler identifies repeated back-to-back acquisitions on the same monitor, it merges them into a single outer synchronized block to eliminate redundant lock/unlock roundtrips.

4 Tricky Questions

1. Why are wait(), notify(), and notifyAll() defined in java.lang.Object instead of java.lang.Thread?

Answer:

  1. Encapsulation of Shared State: In concurrent programming, threads wait for conditions on shared data, not for specific threads. Shared data is encapsulated within arbitrary Java objects.
  2. Monitor Binding: The methods operate on the WaitSet of the specific object’s intrinsic monitor. If these methods were on Thread, the JVM would have no way of knowing which monitor the thread intends to relinquish, especially if the thread holds multiple nested locks simultaneously.
  3. Invoking lock.wait() releases only the monitor of lock, leaving any other outer synchronized locks held by the thread fully locked.

2. What happens to a monitor if an unhandled OutOfMemoryError or runtime exception is thrown inside a synchronized block?

Answer: The monitor is guaranteed to be released.

The javac compiler generates an implicit exception table (Exception table) with target type any that wraps all bytecode instructions within the critical section. Any unhandled Throwable or fatal Error routes execution to an exceptional handler path that invokes monitorexit on the monitor reference before executing athrow. The only scenario where the monitor is not freed is an abnormal JVM termination via SIGKILL or an unrecoverable crash in native JNI code.


3. Where exactly does a thread move when notify() is invoked: CPU execution, _EntryList, or does it remain in _WaitSet?

Answer: The notified thread does not begin executing on a CPU core immediately, nor does it immediately compete for the lock:

  1. Invoking notify() selects one thread from _WaitSet and transfers it either to _EntryList or to _cxq (depending on the HotSpot Knob_MoveToWaitSet heuristic).
  2. The thread transitions from Thread.State.WAITING to Thread.State.BLOCKED.
  3. The notifying thread retains monitor ownership until it explicitly exits its own synchronized block (monitorexit). Only after the monitor is fully vacated can the awakened thread contend for lock ownership against incoming threads.

4. How does calling Object.hashCode() affect an object currently locked by a Lightweight Lock?

Answer: Calling Object.hashCode() or System.identityHashCode() on an object held under a Lightweight Lock causes immediate and forced Lock Inflation to a Heavyweight Lock (ObjectMonitor).

During Lightweight Locking, the object’s Mark Word is replaced with a pointer to a Lock Record on the owning thread’s stack. The stack Lock Record does not have allocated space to store a generated 31-bit identity hash code permanently. To prevent losing the identity hash code upon unlock, the JVM inflates the lock to an ObjectMonitor, storing the hash code inside ObjectMonitor._header.


🎯 Interview Cheat Sheet

Core Concepts Matrix

| Dimension | Lightweight Lock (Thin) | Heavyweight Lock (Fat / ObjectMonitor) | | :— | :— | :— | | Mark Word Tag | 00 | 10 | | Lock Storage | Stack Lock Record | Native C++ ObjectMonitor heap structure | | Acquisition Mechanism| Atomic CAS (lock cmpxchg) | OS mutex / syscall (futex / WaitForSingleObject)| | Cost | 2–5 ns (Zero syscall overhead) | 1–5 µs (OS context switch & thread descheduling)| | Contention State | Zero contention (Single thread) | Contested by multiple concurrent threads |


Key Takeaways

  • Dual Responsibilities: Monitors provide Mutual Exclusion (synchronized) and Thread Signaling (wait/notify).
  • Escalation Path: Unlocked (01) $\to$ Lightweight Lock (00) $\to$ Heavyweight Lock (10).
  • Java 21 Invariant: Biased Locking is permanently removed (JEP 416); synchronization starts at Lightweight Locking.
  • Spurious Wakeup Protection: Always invoke wait() inside a while (condition) loop, never inside an if block.

Red Flags & Anti-Patterns to Avoid

  • ❌ Assuming wait() releases all held locks: wait() releases only the specific monitor on which it was called, leaving outer locks held (common source of deadlocks).
  • ❌ Using if instead of while with wait(): Exposes applications to race conditions and spurious wakeups.
  • ❌ Believing monitors are separate heap objects allocated for every Java instance: Monitors are conceptual primitives encoded directly into the object’s 64-bit Mark Word.
  • ❌ Thinking notify() immediately runs the target thread: The target thread moves to BLOCKED and must wait for the current owner to release the lock.