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:
- 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
synchronizedkeyword). - Cooperation and Signaling: Enables threads to coordinate work by relinquishing lock ownership and waiting for specific conditions to become true via
wait(),notify(), andnotifyAll().
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 theThread.State.BLOCKEDstate. - Wait Set (
WaitSet): The collection of threads that invokedwait()while holding the monitor. They voluntarily released the lock and are awaiting an explicit wake-up signal (notify()/notifyAll()). These threads reside in theThread.State.WAITINGorTIMED_WAITINGstate.
+------------------------------------+
| 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:
- Klass Word: A pointer to the class metadata in Metaspace (4 bytes with compressed class pointers enabled, otherwise 8 bytes).
- 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_cxqinto_EntryListand 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
javaccompiler generates an internalException tablecovering the critical section with target typeany. If an unhandledThrowableorErroroccurs, control routes to offset 13, executingmonitorexitbefore 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, aBasicObjectLock(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
nullDisplaced 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:
- The CAS fails.
- The contending thread executes Adaptive Spinning, spinning in a tight loop across CPU
PAUSEinstructions, betting that the owner will finish shortly. - If spinning expires without acquiring the lock, Lock Inflation triggers:
- The JVM allocates a native C++
ObjectMonitorstructure. - The Displaced Mark Word is moved into
ObjectMonitor._header. - The object’s Mark Word is updated via CAS to point to the
ObjectMonitorwith state bits set to10. - The contending thread adds itself to
_cxqand parks via an OS kernel syscall (futexon Linux orWaitForSingleObjecton Windows), transitioning toThread.State.BLOCKED.
- The JVM allocates a native C++
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 idleObjectMonitorinstances 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 nativeObjectMonitormemory 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()orSystem.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 HeavyweightObjectMonitor, because onlyObjectMonitor._headercan permanently preserve both the identity hashcode and the lock state simultaneously.
JIT Compiler Optimizations
The HotSpot C2 compiler optimizes synchronized code aggressively:
- 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
monitorenterandmonitorexitfrom 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 } - 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:
- 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.
- Monitor Binding: The methods operate on the
WaitSetof the specific object’s intrinsic monitor. If these methods were onThread, the JVM would have no way of knowing which monitor the thread intends to relinquish, especially if the thread holds multiple nested locks simultaneously. - Invoking
lock.wait()releases only the monitor oflock, 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:
- Invoking
notify()selects one thread from_WaitSetand transfers it either to_EntryListor to_cxq(depending on the HotSpotKnob_MoveToWaitSetheuristic). - The thread transitions from
Thread.State.WAITINGtoThread.State.BLOCKED. - The notifying thread retains monitor ownership until it explicitly exits its own
synchronizedblock (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 awhile (condition)loop, never inside anifblock.
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
ifinstead ofwhilewithwait(): 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 toBLOCKEDand must wait for the current owner to release the lock.