What is monitor in Java?
A monitor is a synchronization mechanism in Java that provides mutual exclusion. Every object in Java has its own built-in (intrinsic) monitor.
Junior Level
Basic Understanding
A monitor is a synchronization mechanism in Java that provides mutual exclusion. Every object in Java has its own built-in (intrinsic) monitor.
Intrinsic monitor — every Java object has a monitor “out of the box”. The JVM automatically associates a data structure for synchronization with each object. That’s why any object can be used as a lock key.
Simple Analogy
Imagine a meeting room with one door:
- Monitor = the meeting room
- Key = the lock
- Queue outside = threads waiting to enter (EntryList)
- Waiting room = threads that called wait() (WaitSet)
Usage Example
public class Counter {
private int count = 0;
// synchronized uses the monitor of object 'this'
public synchronized void increment() {
count++; // Only one thread can be here
}
public synchronized int getCount() {
return count;
}
}
Equivalent Forms
// Option 1: synchronized method
public synchronized void method() {
// code
}
// Option 2: synchronized block (same thing)
public void method() {
synchronized(this) { // Acquire monitor of object 'this'
// code
} // Release monitor
}
wait() and notify()
Monitor methods for inter-thread communication:
synchronized(lock) {
while (!condition) {
lock.wait(); // Releases monitor and waits
}
// Continues when notify() is called
}
// In another thread:
synchronized(lock) {
condition = true;
lock.notify(); // Wakes up one waiting thread
}
Important Rules
| Rule | Description |
|---|---|
| Only one owner | Only one thread can own the monitor at a time |
| Reentrancy | Owner can re-enter the monitor without blocking |
| wait() releases | Calling wait() temporarily releases the monitor |
| Only inside synchronized | wait()/notify() only work inside a synchronized block |
Middle Level
Connection to Object Header (Mark Word)
Every object in Java has a header containing the Mark Word:
Mark Word — part of the object header in the JVM. Stores lock information: which thread owns it, state (unlocked, biased, thin, fat). On 64-bit JVM = 8-12 bytes.
|--------------------------------------------------------------|
| Object Header |
| -------------------------------------------------------------- | ------------------------- |
| Mark Word (64/32 bit) | Class Pointer (64/32 bit) |
| -------------------------------------------------------------- | |
The Mark Word stores lock state bits:
| Bits (64-bit JVM) | State |
|---|---|
01 |
No Lock / Biasable |
01 |
Biased Locking (with Thread ID) |
00 |
Lightweight Lock (Thin Lock) |
10 |
Heavyweight Lock (Fat Lock) |
11 |
Marked for GC |
ObjectMonitor Structure (C++ JVM level)
When a monitor is “inflated” to heavyweight state, the Mark Word contains a pointer to ObjectMonitor:
// Simplified structure from HotSpot
class ObjectMonitor {
void* _owner; // Owner thread
ObjectWaiter* _EntryList; // Queue of BLOCKED threads
ObjectWaiter* _WaitSet; // Queue of WAITING threads
int _Recursion; // Reentrancy counter
int _WaitSetLock; // WaitSet protection
};
Thread Lifecycle in a Monitor
┌─────────────────────────────────┐
│ │
▼ │
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐
│ NEW │→│ EntryList │→│ OWNER │→│ EXIT │
│ (created)│ │ (BLOCKED) │ │(RUNNING) │ │ (released) │
└──────────┘ └──────────────┘ └──────────┘ └──────────────┘
│
│ wait()
▼
┌──────────────┐
│ WaitSet │
│ (WAITING) │
└──────────────┘
│
│ notify()
▼
┌──────────────┐
│ EntryList │
│ (BLOCKED) │
└──────────────┘
Monitor Operations
| Method | Description |
|---|---|
monitor.enter() |
Acquire monitor (blocks if busy) |
monitor.exit() |
Release monitor |
wait() |
Releases monitor and transitions to WAITING |
notify() |
Moves one thread from WaitSet to EntryList |
notifyAll() |
Moves all threads from WaitSet to EntryList |
Lock Record on the Stack
Lock Record — a data structure on the thread’s stack that stores a copy of the Mark Word when acquiring a lock. Needed to restore the Mark Word on release.
For lightweight locks, the JVM avoids creating heavy ObjectMonitor:
- JVM copies the object’s Mark Word to the Lock Record on the thread’s stack
- Thread uses fast CAS operations to acquire
- If no contention — everything stays on the stack (very fast)
- If contention appears — Lock Inflation to Heavyweight occurs
Lock Inflation — transition from light locking (biased/thin) to heavy (fat lock). Happens under contention: when multiple threads try to acquire the same monitor.
When NOT to use monitors
- High-throughput, write-heavy: millions of ops/sec — lock-free algorithms (Atomic, CHM) are faster
- Read-heavy load: ReentrantReadWriteLock allows parallel reads, monitor does not
- Need tryLock with timeout: monitor blocks forever, ReentrantLock.tryLock(timeout) does not
Senior Level
Under the Hood: Lock State Evolution
The JVM (HotSpot) uses a lock escalation strategy (Lock Inflation):
1. No Lock (001)
|---------------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | bias:0 | 01 |
| --------------------------------------------------------------- | -------------------- | -------- | ----- | ------ | --- |
Object is not locked. Mark Word contains hash code and object age (GC age).
2. Biased Locking
|-------------------------------------------------------------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | bias:1 | 01 |
| ------------------------------------------------------------------------- | ------- | -------- | ----- | ------ | --- |
- If an object is only acquired by one thread, the JVM “biases” it toward that thread
- Thread enters synchronized without CAS, just checking the thread ID in the header
- Biased Locking — disabled by default since Java 15 (JEP 374), fully removed in Java 21 (JEP 416). In modern Java versions, monitoring goes: No Lock → Thin Lock → Fat Lock.
3. Thin Lock (lightweight lock)
|---------------------------------------------|
| thread:54 | lock record:6 | 00 |
| --------------------------------------------- | ------------- | --- |
- Under contention, CAS is used to replace the Mark Word with a pointer to the Lock Record
- Thread does not sleep — performs adaptive spinning (a few empty cycles)
4. Fat Lock (heavyweight lock)
|----------------------------------------------|
| object monitor pointer:62 | 10 |
| ---------------------------------------------- | --- |
- Under high contention, a full
ObjectMonitoris created - Thread is parked via the OS (
futexin Linux,WaitForSingleObjectin Windows) — BLOCKED state - Context Switch — very expensive (~1000+ ns)
Adaptive Spinning
HotSpot JVM remembers lock history:
// JVM tracks:
// - How many times spinning succeeded on this monitor
// - How many times the thread parked immediately
// If spinning succeeded — spin longer
// If not — park the thread immediately
This dramatically improves performance for short-lived locks.
Monitor Inflation and Deflation
No Lock → Biased → Thin → Fat (Inflation — happens automatically)
Fat → Thin → ... (Deflation — only during Safe Points / GC)
Many Fat Locks → performance drops due to context switches.
JIT Compiler Optimizations
Lock Coarsening
// Before optimization:
synchronized(lock) { list.add(a); }
synchronized(lock) { list.add(b); }
synchronized(lock) { list.add(c); }
// After Lock Coarsening:
synchronized(lock) {
list.add(a);
list.add(b);
list.add(c);
}
Lock Elision (via Escape Analysis)
public void method() {
Object lock = new Object(); // Object doesn't "escape" the method
synchronized(lock) {
// JIT will completely remove this lock!
// Because no one else can access lock
}
}
Performance and Highload
Contention and EntryList
If hundreds of threads try to acquire one monitor:
EntryListbecomes a bottleneck- Threads constantly context-switch
- CPU spends more time switching than working
Solutions:
- Segmented locks (like in
ConcurrentHashMap) - Use
LongAdderinstead ofAtomicInteger - ReadWriteLock when reads dominate writes
Diagnostics
jstack -l
jstack -l <pid>
Output:
"worker-1" #15 prio=5 os_prio=0 tid=0x00007f... nid=0x1234 waiting for monitor entry
- waiting to lock <0x000000076af0c8d0> (a java.lang.Object)
- locked <0x000000076af0c8e0> (a java.util.HashMap)
"worker-2" #16 prio=5 os_prio=0 tid=0x00007f... nid=0x1235 waiting for monitor entry
- waiting to lock <0x000000076af0c8e0> (a java.util.HashMap)
JMC (Java Mission Control)
- Event
Java Monitor Wait— time in wait() - Event
Java Monitor Enter— time in BLOCKED - Shows specific objects with the highest contention
javap -c
javap -c MyClass.class
public void method();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // Acquire monitor
4: ...
20: aload_1
21: monitorexit // Normal exit
22: goto 30
25: astore_2
26: aload_1
27: monitorexit // Exit on exception
28: aload_2
29: athrow
30: return
Best Practices
- Minimize lock scope — keep synchronized blocks as short as possible
- Use private final Object lock — encapsulation, no external code can grab your lock
- Avoid class-level locks —
synchronized(ClassName.class)is very dangerous - Consider ReadWriteLock — if reading dominates writing
- Monitor contention via JMC — growing wait time = problem
- Avoid synchronized in high-load systems — prefer lock-free structures
Interview Cheat Sheet
Must know:
- Every Java object has a built-in (intrinsic) monitor — a mutual exclusion mechanism
- Monitor = EntryList (queue of BLOCKED threads) + WaitSet (queue of WAITING threads) + reentrancy counter
- Mark Word in object header stores lock state: No Lock → Thin → Fat (escalation)
- Biased Locking removed in Java 21; modern path: No Lock → Thin Lock → Fat Lock
- wait() temporarily releases the monitor and moves the thread to WaitSet; notify() moves it to EntryList
- JVM optimizes: Lock Coarsening (merging), Lock Elision (removal via Escape Analysis)
- Under high contention, Thin Lock “inflates” to Fat Lock with ObjectMonitor (context switch ~1000+ ns)
Frequent follow-up questions:
- What happens when synchronized is called? — JVM tries to acquire the monitor: first biased → thin (CAS + spinning) → fat (ObjectMonitor + OS parking)
- Can you call wait() without synchronized? — No, IllegalStateException — wait/notify only work inside a synchronized block
- What is a Lock Record? — A structure on the thread’s stack with a copy of the Mark Word; used in Thin Locking
- When is a monitor NOT suitable? — High-throughput write-heavy loads; better to use lock-free (Atomic, ConcurrentHashMap)
Red flags (do NOT say):
- “Monitor is a separate object in memory” — no, it’s built into every object (intrinsic)
- “wait() releases the monitor forever” — no, temporarily; the thread returns to EntryList after notify()
- “Synchronized always uses ObjectMonitor” — no, under low contention it uses thin lock on the stack
- “Biased Locking is still relevant in Java 21” — no, fully removed (JEP 416)
Related topics:
- [[5. How does synchronized work at monitor level]] — detailed monitor acquisition algorithm
- [[6. What is the difference between synchronized method and synchronized block]] — different ways to use the monitor
- [[7. What is reentrant lock]] — ReentrantLock as an alternative to the built-in monitor
- [[1. What is the difference between synchronized and volatile]] — when you need a monitor vs volatile