Understanding the Implementation Principles of Java's Synchronized Mechanism

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:

  1. The thread tries to acquire the lock by checking the Monitor's Owner field
  2. If the Owner is null, the thread becomes the Owner, increments the reentrancy counter, and proceeds to execute the critical section
  3. If the Owner is the current thread, it simply increments the reentrancy counter (demonstrating reentrancy)
  4. If the Owner is another thread, the thread is added to the Entry List and transitions to the BLOCKED state
  5. When the executing thread completes the critical section (either normally or via exception), it decrements the reentrancy counter
  6. 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
  7. 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 monitorexit is 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

  1. Prefer synchronized: For most scenarios involving simple synchronization or low concurrency, synchronized is sufficient. It's automatically optimized by the JVM and requires no manual management, reducing the chance of errors.
  2. 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

Tags: java Synchronized ReentrantLock Concurrency multithreading

Posted on Mon, 01 Jun 2026 17:30:19 +0000 by hws