How the Java Synchronized Keyword Works with Object Monitors and Thread Safety

The Java synchronized keyword ensures thread safety by requiring a thread to acquire an object monitor (intrinsic lock) before executing a block of code or method, establishing happens-before relationships that guarantee visibility of memory writes across threads.

The synchronized keyword is Java's fundamental tool for concurrent programming, providing mutual exclusion and memory visibility guarantees through object monitors. According to the CyC2018/CS-Notes repository, understanding how this mechanism interacts with the Java Memory Model (JMM) is essential for writing correct multithreaded applications. This article explores the internal workings of the Java synchronized keyword, from monitor acquisition to bytecode-level implementation.

How Object Monitors Work in Java

Every object in Java has an associated object monitor (also called an intrinsic lock or mutex), including class objects used for static synchronization. When a thread enters a synchronized block or method, it must first acquire this monitor.

Monitor Acquisition and the Java Memory Model

When a thread reaches a synchronized (obj) { … } block or a synchronized method, the JVM attempts to acquire obj’s monitor. This corresponds to the monitor lock operation defined in the Java Memory Model (JMM)—formally called lock.

The acquiring thread establishes a happens-before relationship with any later release of the same monitor. All writes performed before the lock are guaranteed to be visible to the thread that later acquires the monitor. The JVM inserts a memory barrier on entry (monitorenter) that flushes any cached values from the thread’s working memory back to main memory, guaranteeing visibility of prior writes.

Monitor Release and Visibility Guarantees

When the synchronized block or method finishes—whether normally or via an exception—the JVM automatically executes a monitor exit (monitorexit). This releases the lock and inserts a store barrier, pushing the thread’s updates to main memory.

The unlock operation in the JMM creates a happens-before edge to any subsequent lock on the same monitor, making the changes visible to other threads. This mechanism ensures that synchronized provides both mutual exclusion and memory consistency without requiring additional volatile declarations.

Re-entrancy and Recursion Counting

Java monitors are re-entrant. A thread that already owns a monitor may acquire it again without blocking. The JVM maintains an internal recursion count (lock count) and only truly releases the monitor when the count reaches zero.

This re-entrancy is part of the JVM’s monitor implementation and does not break the JMM ordering guarantees.

Synchronized Usage Patterns and Code Examples

Synchronized Instance Methods

A synchronized instance method uses the current object (this) as its monitor. This is equivalent to wrapping the entire method body in synchronized (this).

public class Counter {
    private int value = 0;

    public synchronized void increment() {
        value++;
    }

    public synchronized int get() {
        return value;
    }
}

The this monitor ensures that only one thread can execute increment() or get() at a time for a given instance.

Synchronized Static Methods and Class-Level Monitors

Static synchronized methods use the class object as their monitor, providing synchronization across all instances of the class.

public class GlobalConfig {
    private static int version = 0;

    public static synchronized void bump() {
        version++;
    }

    public static synchronized int current() {
        return version;
    }
}

Here, the monitor is GlobalConfig.class, ensuring thread safety for static state.

Explicit Synchronized Blocks

For finer-grained control, you can synchronize on any object, allowing multiple independent locks within a single class.

public class Counter {
    private int value = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            value++;
        }
    }

    public int get() {
        synchronized (lock) {
            return value;
        }
    }
}

Explicit blocks reduce lock contention by protecting only the critical sections rather than entire methods.

Coordinating Threads with wait() and notify()

The object monitor also maintains a wait-set for condition-based coordination. When a thread calls wait(), it temporarily releases the monitor and enters the wait-set until another thread invokes notify() or notifyAll().

public class BoundedBuffer {
    private final Queue<Integer> queue = new ArrayDeque<>();
    private final int capacity = 5;

    public synchronized void put(int x) throws InterruptedException {
        while (queue.size() == capacity) {
            wait();  // releases monitor, waits
        }
        queue.add(x);
        notifyAll();  // wakes waiting consumers
    }

    public synchronized int take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();  // releases monitor, waits
        }
        int v = queue.remove();
        notifyAll();  // wakes waiting producers
        return v;
    }
}

Both put and take synchronize on the same object monitor (this), ensuring proper coordination between producer and consumer threads.

Re-entrant Locking Example

Java's monitors support re-entrancy, allowing a thread to acquire the same lock multiple times without deadlock.

public synchronized void outer() {
    System.out.println("outer");
    inner();  // inner() also synchronized on this
}

public synchronized void inner() {
    System.out.println("inner");
}

The thread that entered outer() already holds the monitor, so calling inner() does not block—it simply increments the internal lock count.

JVM Implementation and Bytecode Details

According to the source analysis in notes/Java 虚拟机.md, the JVM implements synchronization through two bytecode instructions:

  • monitorenter: Executed when entering a synchronized block, attempting to acquire the object's monitor
  • monitorexit: Executed when exiting, releasing the monitor (inserted by the compiler even for exception paths)

These bytecodes correspond directly to the lock and unlock operations defined in the Java Memory Model, as detailed in notes/Java 并发.md (sections 33-55). The JVM handles the heavy lifting of queueing blocked threads, maintaining recursion counts, and inserting the necessary memory barriers to ensure visibility.

Synchronized vs. Explicit Locks (ReentrantLock)

While java.util.concurrent.locks.ReentrantLock provides additional features like try-lock, timed lock acquisition, and fair ordering, the synchronized keyword remains the preferred choice for most use cases due to its simpler semantics and automatic resource management. As implemented in the JVM, synchronized uses the same underlying monitor concepts when interacting with wait() and notify(), but requires less boilerplate and eliminates the risk of forgetting to release the lock.

Summary

  • The Java synchronized keyword works by requiring threads to acquire an object monitor (intrinsic lock) before entering protected code, ensuring mutual exclusion.
  • Monitor acquisition and release establish happens-before relationships in the Java Memory Model, guaranteeing visibility of memory writes across threads without requiring volatile.
  • The JVM implements this mechanism using monitorenter and monitorexit bytecodes, with automatic memory barriers ensuring cache coherence.
  • Java monitors are re-entrant, allowing threads to recursively acquire locks they already hold using an internal recursion count.
  • Instance methods synchronize on this, static methods on the class object, and blocks can use any object as a monitor.
  • The monitor's wait-set enables condition-based coordination through wait(), notify(), and notifyAll(), where waiting threads temporarily release the monitor.

Frequently Asked Questions

What is the difference between synchronized methods and synchronized blocks?

Synchronized methods implicitly use the object instance (this for instance methods) or the class object (for static methods) as their monitor, locking the entire method body. Synchronized blocks allow you to specify any object as the monitor and restrict the locked region to a specific code segment, providing finer-grained concurrency control and potentially reducing lock contention.

How does the Java Memory Model ensure visibility with synchronized?

The JMM defines lock and unlock actions that correspond to monitor acquisition and release. When a thread releases a monitor (exits synchronized code), it creates a happens-before edge to any subsequent acquisition of that same monitor by another thread. This guarantees that all memory writes made by the releasing thread become visible to the acquiring thread, ensuring visibility without requiring explicit volatile declarations.

Can a thread acquire the same monitor multiple times?

Yes, Java monitors are re-entrant. A thread that already holds a monitor can acquire it again without blocking or deadlocking. The JVM maintains an internal recursion count (lock count) for each monitor, incrementing it on re-acquisition and decrementing it on release. The monitor is only fully released when the count reaches zero, allowing other threads to acquire it.

What happens when a thread calls wait() inside a synchronized block?

When a thread invokes wait() on an object inside a synchronized block, it temporarily releases the object's monitor and adds itself to that monitor's wait-set, entering a waiting state. The thread remains blocked until another thread calls notify() or notifyAll() on the same object. Upon waking, the thread must re-acquire the monitor before it can continue execution, ensuring proper synchronization when it resumes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →