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:
- Bytecode Level:
synchronizedblocks compile into explicitmonitorenterandmonitorexitinstructions, paired with a syntheticException tableto ensure lock release even upon uncaught exceptions.synchronizedmethods do not emitmonitorenter/monitorexitbytecodes; instead, they set theACC_SYNCHRONIZEDaccess flag in the class method table, delegating lock management to JVM stack frame invocation and unwinding.
- 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 theBLOCKEDstate.
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.Classinstance. - 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:
- 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 Recordon the thread stack, but storesnullas its Displaced Mark Word to indicate nesting. - Each
monitorexitpops the topLock Record. Only when the finalLock Recordcontaining the non-null Displaced Mark Word is popped does the JVM execute the CAS to release the lock.
- The first acquisition pushes a
- Heavyweight Locking (Fat Lock):
- In the C++
ObjectMonitorstructure, the_recursionscounter increments on each nested entry. - On
monitorexit,_recursionsdecrements. The lock is only released to other threads when_recursions == 0.
- In the C++
🔴 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:
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.
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.
- 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_cxqwithout acquiring any central lock._EntryList: A doubly-linked list. When the current owner releases the lock, it drains threads from_cxqinto_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:
- Lock Elision (Escape Analysis):
If Escape Analysis confirms an object has
NoEscapestatus (confined strictly to the thread/method stack), the compiler strips allmonitorenterandmonitorexitinstructions 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! } - 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
synchronizedblock, thejavaccompiler emits an explicitException tableentry covering the instructions within the critical section. It generates an exceptional path with a duplicatedmonitorexitinstruction followed byathrow. - In a
synchronizedmethod, noException tableis generated for the monitor. Lock acquisition and release are handled implicitly by the JVM execution engine via theACC_SYNCHRONIZEDflag. 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:
- 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. - Primitive Caching: Values from
Integer.valueOf(-128)toInteger.valueOf(127)are cached by the JVM Integer Cache. Locking onInteger.valueOf(1)locks on a globally shared singleton instance. - 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 throwIdentityException. 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/monitorexitvsACC_SYNCHRONIZED) work in tandem with JVM HotSpot Mark Word manipulation. - Escalation Path (Java 21): Lightweight Lock (
00) $\to$ Adaptive Spinning $\to$ Heavyweight Lock (10viaObjectMonitor). Biased locking is dead. - JMM Boundaries:
monitorenterprovides Acquire semantics;monitorexitprovides Release semantics. - Uninterruptible: Waiting in
BLOCKEDfor 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
synchronizedmethods are faster because of less bytecode: Modern JIT compilers yield identical execution performance; blocks provide superior architectural encapsulation. - ❌ Believing
Thread.sleep()releases monitor locks: OnlyObject.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.