Understanding the Internal Implementation of synchronized (Deep Dive with Monitor)
Core Premise: Locks are Bound to Objects
The synchronized keyword in Java, whether applied to methods or code blocks, ultimately associates with a specific object. Instance methods bind to the current object instance (this), static methods bind to the Class object, and code blocks bind to a specified object. The Monitor (also known as a monitor or mutex) is the fundamental mechanism that implements synchronization at the JVM level.
What is a Monitor?
A Monitor is a synchronization construct implemented at the JVM level, essentially an object (implemented in C++) that contains several key components:
| Component | Function |
|---|---|
| Owner | References the thread currently holding the lock, initially null |
| Wait Set | Contains threads that have called wait() and are in the WAITING state |
| Entry List | Holds threads that failed to acquire the lock and are in the BLOCKED state |
| Reentrancy Counter | Tracks the number of times the lock has been reacquired (supports reentrancy), starts at 0, increments on lock acquisition, decrements on release |
How synchronized Interacts with Monitor
When a thread attempts to execute synchronized code, the following process occurs:
- The thread tries to acquire the lock by checking the Monitor's Owner field
- If the Owner is null, the thread becomes the Owner, increments the reentrancy counter, and proceeds to execute the critical section
- If the Owner is the current thread, it simply increments the reentrancy counter (demonstrating reentrancy)
- If the Owner is another thread, the thread is added to the Entry List and transitions to the BLOCKED state
- When the executing thread completes the critical section (either normally or via exception), it decrements the reentrancy counter
- If the counter reaches zero, the Owner is set to null and one thread from the Entry List is awakened to attempt acquiring the lock
- If the counter is still greater than zero, the thread continues holding the lock and proceeds with execution
Key Implementation Details:
- Reentrancy: The same thread can acquire the same lock multiple times without causing deadlock, as only the reentrancy counter is incremented
- Release Mechanism: The lock is automatically released when the critical section is completed or when an exception is thrown (JVM ensures
monitorexitis always executed) - Lock Escalation:
- In the bias lock and lightweight lock stages, the complete Monitor is not utilized; synchronization is achieved through the object's Mark Word and CAS operations
- When upgraded to a heavyweight lock, the full Monitor is engaged, and threads that fail to compete for the lock enter the Entry List in a blocked state
Low-Level Implementation of synchronized
| Syntax | Bytecode Implementation | Monitor Interaction |
|---|---|---|
| Synchronized block | monitorenter / monitorexit instructions |
Execute monitorenter to acquire the Monitor, monitorexit to release |
| Synchronized instance method | ACC_SYNCHRONIZED flag |
JVM automatically acquires/releases Monitor for the this object |
| Synchronized static method | ACC_SYNCHRONIZED flag |
JVM automatically acquires/releases Monitor for the Class object |
Comparing synchronized and ReentrantLock
ReentrantLock, found in the java.util.concurrent.locks package, is an explicit lock implementation. Unlike synchronized (an implicit lock), it provides more advanced synchronization features. Here's a detailed comparison:
| Comparison Aspect | synchronized | ReentrantLock |
|---|---|---|
| Lock Type | Implicit lock (JVM level), automatically acquired/released | Explicit lock (API level), requires manual lock()/unlock() (should be placed in finally block) |
| Reentrancy | Supported (JVM automatically maintains reentrancy counter) | Supported (manually maintained, defaults to non-fair lock, fair lock can be specified) |
| Fairness | Non-fair (cannot be modified) | Supports both fair and non-fair locks (specify new ReentrantLock(true) for fair) |
| Lock Wait Interruption | Not supported (waiting threads cannot be interrupted) | Supported (lockInterruptibly() allows interruption of waiting threads) |
| Timeout for Lock Acquisition | Not supported (threads block indefinitely) | Supported (tryLock(long timeout, TimeUnit) abandons after timeout) |
| Condition Variables | Only one (Object's wait/notify) | Multiple support (newCondition() creates multiple Conditions for precise thread awakening) |
| Performance | Optimized after JDK 1.6 (lock escalation), similar to ReentrantLock | Slightly better under high concurrency (flexible control), but requires manual release |
| Exception Handling | Automatic release (even if exception occurs) | Must be released in finally block, otherwise deadlock may occur |
| Usage Complexity | Simple (no manual management required) | Complex (requires manual control, prone to errors) |
Code Examples for Key Differences
1. Basic ReentrantLock Usage (Explicit Lock)
import java.util.concurrent.locks.ReentrantLock;
public class ExplicitLockDemo {
private static final ReentrantLock lock = new ReentrantLock(true); // Fair lock
public static void processTask() {
lock.lock(); // Manually acquire lock
try {
System.out.println("Thread " + Thread.currentThread().getName() + " is processing");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Thread " + Thread.currentThread().getName() + " was interrupted");
} finally {
lock.unlock(); // Must release in finally to prevent deadlock
}
}
public static void main(String[] args) {
new Thread(ExplicitLockDemo::processTask, "Worker-1").start();
new Thread(ExplicitLockDemo::processTask, "Worker-2").start();
}
}
2. ReentrantLock with Timeout and Interruption
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class AdvancedLockDemo {
private static final ReentrantLock lock = new ReentrantLock();
public static void attemptLockWithTimeout() {
try {
// Attempt to acquire lock with timeout: 3 seconds
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " acquired lock successfully");
Thread.sleep(5000); // Simulate long-running operation
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " failed to acquire lock after timeout");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " was interrupted while waiting for lock");
Thread.currentThread().interrupt(); // Restore interrupt flag
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(AdvancedLockDemo::attemptLockWithTimeout, "Task-A");
Thread t2 = new Thread(AdvancedLockDemo::attemptLockWithTimeout, "Task-B");
t1.start();
Thread.sleep(1000); // Let first thread acquire the lock
t2.start();
t2.interrupt(); // Interrupt second thread's wait
}
}
Sample Output:
Task-A acquired lock successfully
Task-B was interrupted while waiting for lock
Guidelines for Usage Selection
- Prefer synchronized: For most scenarios involving simple synchronization or low concurrency,
synchronizedis sufficient. It's automatically optimized by the JVM and requires no manual management, reducing the chance of errors. - Choose ReentrantLock when:
- Fair lock behavior is required
- The ability to interrupt a thread waiting for a lock is needed
- Timeout-based lock acquisition is necessary
- Multiple condition variables are required for precise thread awakening
- More flexible lock control is needed in high-concurrency scenarios