Detailed Explanation of Object Monitor (Monitor) and Synchronization Primitive Synchronized Underlying Implementation in Java

Detailed Explanation of Object Monitor (Monitor) and Synchronization Primitive Synchronized Underlying Implementation in Java

1. Knowledge Description
In Java concurrent programming, the synchronized keyword is the core mechanism for achieving thread synchronization and ensuring data consistency. The underlying core concept is the Object Monitor, which is a classic thread synchronization tool. Simply put, each Java object is associated with a monitor at the JVM level. synchronized achieves mutually exclusive access to critical sections (synchronized code blocks/methods) precisely by acquiring and releasing this monitor. Understanding the working mechanism of the object monitor is key to gaining a deep understanding of Java synchronization principles.

2. Step-by-Step Problem Solving/Explanation Process

Step One: Basic Association Between Object Header and Monitor

  1. Object Memory Layout: In the HotSpot JVM, an object's storage layout in heap memory is divided into three parts: object header, instance data, and alignment padding.
  2. Object Header is Key: The object header is the primary structure carrying the object monitor information. It mainly consists of two parts:
    • Mark Word: Used to store the object's own runtime data, such as hash code, GC generation age, lock status flags, etc. It is also the key to implementing the lock (monitor).
    • Klass Pointer: A pointer to the object's metadata (i.e., its class).
  3. Lock State: The flag bits (Lock Bits) in the Mark Word indicate the object's lock state. In a 64-bit JVM, the Mark Word is typically 64 bits, and its stored content changes according to the lock state. Main states include: unlocked (01), biased lock (01), lightweight lock (00), heavyweight lock (10), and GC mark (11), among others.

Step Two: How an Object Associates with the Real Monitor

  1. Heavyweight Lock's Monitor: When we refer to the classic "Object Monitor," we usually specifically mean the synchronization mechanism in the heavyweight lock state. It is a complex synchronization structure built from operating system-provided primitives like mutexes and condition variables.
  2. Association Process: When a thread attempts to acquire an object's lock (enter a synchronized block) and competition escalates to a heavyweight lock, the JVM creates an ObjectMonitor object (implemented in C++) in the heap. This ObjectMonitor is the concrete implementation of the Monitor mechanism.
  3. Pointer Storage: At this point, the object's Mark Word no longer stores the original hash code or age information but is replaced by a pointer to this ObjectMonitor object. This operation is completed through a CAS (Compare and Swap) atomic operation. From then on, this Java object is bound to a heavyweight Monitor.

Step Three: Classic ObjectMonitor Structure and Its Working Principle
The core structure and work queues of the heavyweight lock's ObjectMonitor (in HotSpot source code: ObjectMonitor.hpp) are as follows:

  • _owner: Points to the thread holding this monitor (lock). Initially NULL.
  • _WaitSet: Wait set. Threads that call the wait() method are placed in this queue. These threads are waiting for a certain condition (triggered by notify()/notifyAll()).
  • _EntryList: Entry list. Threads that fail to compete for the lock (blocking at the synchronized entry) are placed in this queue.
  • _recursions: Lock reentry count. Because synchronized is reentrant.

Its workflow (simplified) is as follows:

  1. Thread Attempts to Enter (ENTER): When thread T1 wants to enter a code block protected by synchronized(obj), it first attempts to atomically set the _owner field of the ObjectMonitor associated with obj to point to itself.
  2. Successful Acquisition: If _owner is NULL, the setting succeeds, T1 acquires the lock, enters the synchronized code block to execute, and sets _recursions to 1.
  3. Reentry: If _owner is already T1 itself, _recursions is incremented by 1, reflecting reentrancy.
  4. Competition Failure: If _owner is another thread T2, then T1's acquisition attempt fails.
  5. Entering EntryList: T1 will spin (early stage) or directly enter the _EntryList queue to block and wait. Under a heavyweight lock, this blocking is implemented by the operating system kernel's mutex, involving a switch from user mode to kernel mode, which is costly.
  6. Releasing the Lock (EXIT): When the thread holding the lock, T2, exits the synchronized code block, it decrements _recursions by 1. If _recursions becomes 0, it sets _owner to NULL.
  7. Waking Up Competing Threads: T2 will wake up one or more waiting threads from the _EntryList or _WaitSet (depending on different wake-up strategies). The awakened threads will re-attempt to compete for the lock (setting _owner).

Step Four: The Complete Path from the Synchronized Keyword to the Monitor
The execution of a synchronized method at the bytecode and JVM execution levels is as follows:

  1. Bytecode Instructions: For synchronized code blocks, monitorenter and monitorexit instructions are generated after compilation. For synchronized methods, the method's access flag ACC_SYNCHRONIZED is set.
  2. Interpreted Execution: When the interpreter executes the monitorenter instruction or enters an ACC_SYNCHRONIZED method, it calls the JVM's runtime function (e.g., InterpreterRuntime::monitorenter).
  3. Lock Upgrade Process: The JVM does not immediately use the heavyweight ObjectMonitor. To optimize performance, it adopts a lock upgrade strategy:
    • Biased Lock: Assumes only one thread uses the lock. It records the thread ID in the Mark Word, making subsequent entries by that thread as fast as an unlocked state.
    • Lightweight Lock: When there is slight contention, the thread creates a lock record (Lock Record) in its own stack frame and attempts to copy the Mark Word to the lock record and replace it with a pointer to the lock record via CAS. Success means acquiring the lock. This happens in user mode, avoiding kernel switches through spinning.
    • Heavyweight Lock: When lightweight lock spinning fails (increased contention), the lock inflates. At this point, the JVM creates or associates the ObjectMonitor and pushes the thread into _EntryList for true blocking. This is the classic Monitor mechanism described in detail above.
  4. Final Association: In the heavyweight lock state, the mutual exclusion semantics of synchronized are guaranteed by this ObjectMonitor through the operating system's mutual exclusion primitives.

Summary:
The synchronization capability of synchronized is fundamentally based on the Monitor associated with each object. In scenarios with no or low contention, the JVM avoids the costly overhead of the Monitor through optimization techniques like biased locks and lightweight locks. In high-contention scenarios, the lock inflates to a heavyweight lock. At this point, the object points to an ObjectMonitor structure via its Mark Word. This structure, with its _owner, _EntryList, _WaitSet queues and with the support of the operating system kernel, implements mutual exclusion and conditional waiting between threads. Understanding this process reveals the complete path of synchronized from a syntax keyword to underlying system calls.