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

> Discover how the Java synchronized keyword ensures thread safety by utilizing object monitors and intrinsic locks. Learn about happens-before relationships for guaranteed memory visibility across threads.

- Repository: [CyC2018/CS-Notes](https://github.com/CyC2018/CS-Notes)
- Tags: deep-dive
- Published: 2026-02-24

---

**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)`.

```java
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.

```java
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.

```java
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()`.

```java
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.

```java
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.