Back to Synchronization
Q2Question 2 of 8

How does synchronization work with intrinsic monitors?

Key Concepts
  • Every Java object has an intrinsic monitor lock built into the object header's mark word.
  • synchronized acquires the monitor, guaranteeing mutual exclusion: only one thread in the critical section at a time.
  • The monitor also guarantees visibility: changes made inside a synchronized block are visible to the next thread that acquires the same lock.
  • The JVM optimizes monitors through biased locking, lightweight locking (CAS), and heavyweight locking (OS mutex).
  • Each monitor has an entry set (BLOCKED threads) and a wait set (threads that called wait()).
  • wait(), notify(), and notifyAll() must be called while holding the monitor lock, inside a synchronized block.

Definition

Every Java object has an intrinsic monitor lock, and synchronized acquires it to provide mutual exclusion and memory visibility between threads.
  • Intrinsic monitor – every Java object has a built-in monitor lock stored in the object header's mark word. You do not create it explicitly; it exists automatically.
  • Mutual exclusion – when a thread enters a synchronized block, it acquires the monitor lock. No other thread can acquire the same lock until the first thread releases it.
  • Visibility (happens-before) – all memory writes performed inside a synchronized block become visible to the next thread that acquires the same lock. This prevents stale reads.
  • Monitor sets – each monitor has two sets: the entry set (BLOCKED threads waiting to acquire the lock) and the wait set (WAITING threads that called wait()).
  • Lock escalation – the JVM optimizes monitors through three levels: biased locking (zero overhead for single-thread access), lightweight locking (CAS for low contention), and heavyweight locking (OS mutex for high contention).

Every object in Java comes with an invisible padlock. When you enter synchronized(account), you grab the padlock. Only one person can hold the padlock at a time. Others who try to grab it must wait in line (entry set). When you release the padlock (exit the synchronized block), the next person in line grabs it. The padlock also guarantees that anything you wrote on the shared whiteboard inside the locked room is visible to the next person who enters.

Understanding the Concepts

What is an intrinsic monitor?

Every object in Java has a monitor built into its header. In HotSpot JVM, the object header contains a mark word that stores the lock state, hash code, and GC age. When a thread enters a synchronized block on an object, it acquires that object's monitor lock.

JAVA
package com.bank.core;

import java.math.BigDecimal;

public class MonitorDemo {
    private BigDecimal balance = new BigDecimal("100000.00");

    public void debit(BigDecimal amount) {
        // 'this' is the monitor object
        synchronized (this) {
            // Only ONE thread can be here at a time
            balance = balance.subtract(amount);
        }
        // Lock released: next thread can enter
    }
}

How does synchronization prevent race conditions?

Without synchronization, two threads can read the same balance, both compute a debit, and one update overwrites the other. Synchronization forces threads to execute the critical section one at a time.

JAVA
package com.bank.core;

import java.math.BigDecimal;

public class RacePreventionDemo {
    // WRONG: no synchronization
    static class UnsafeAccount {
        BigDecimal balance = new BigDecimal("100000.00");

        void debit(BigDecimal amount) {
            // Thread-1 reads 100000, Thread-2 reads 100000
            // Both subtract 50000, both write 50000
            // Expected: 0. Actual: 50000. Lost update!
            balance = balance.subtract(amount);
        }
    }

    // CORRECT: synchronized
    static class SafeAccount {
        private final Object lock = new Object();
        BigDecimal balance = new BigDecimal("100000.00");

        void debit(BigDecimal amount) {
            synchronized (lock) {
                // Thread-1 acquires lock, reads 100000, writes 50000, releases
                // Thread-2 acquires lock, reads 50000, writes 0, releases
                // Correct: 0
                balance = balance.subtract(amount);
            }
        }
    }
}

What does the monitor guarantee?

The monitor provides three guarantees:

  1. Mutual exclusion: only one thread can hold the lock at a time. Other threads are BLOCKED in the entry set.
  2. Visibility: all writes performed while holding the lock are flushed to main memory on lock release. The next thread that acquires the lock reads from main memory, seeing the latest values.
  3. Atomicity (from the perspective of other threads): the critical section appears to execute as one indivisible unit because no other thread can interleave.

How does the JVM optimize monitors?

The JVM does not always use an expensive OS mutex. It uses a technique called lock escalation.

  • Biased locking: if only one thread ever locks a particular object, the JVM stores that thread's ID in the mark word. Subsequent lock operations by the same thread are essentially free (no CAS, no OS call). Biased locking was deprecated in Java 15 and removed in Java 18.
  • Lightweight locking: when a second thread tries to acquire the lock, the bias is revoked and the JVM uses a CAS (Compare-And-Swap) operation. If the CAS succeeds, the thread owns the lock without an OS call.
  • Heavyweight locking: under high contention (many threads competing), the JVM escalates to an OS mutex. Threads that fail to acquire the lock are parked by the OS, which involves a context switch.

How do wait() and notify() interact with the monitor?

wait() and notify() must be called while holding the monitor lock (inside a synchronized block). wait() releases the lock and moves the thread from the owner to the wait set. notify() moves one thread from the wait set to the entry set.

JAVA
package com.bank.core;

public class MonitorWaitNotifyDemo {
    private static final Object monitor = new Object();
    private static boolean tradeReady = false;

    public static void main(String[] args) throws InterruptedException {
        Thread consumer = new Thread(() -> {
            synchronized (monitor) {
                while (!tradeReady) {
                    try { monitor.wait(); } // releases lock, enters wait set
                    catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println("Consumer: processing trade");
            }
        });

        consumer.start();
        Thread.sleep(200);

        synchronized (monitor) {
            tradeReady = true;
            monitor.notify(); // moves consumer from wait set to entry set
        }
        consumer.join();
    }
}
TEXT
Consumer: processing trade

What are the rules for using monitors?

  • wait(), notify(), and notifyAll() must be called inside a synchronized block on the same object. Otherwise, IllegalMonitorStateException is thrown.
  • Always use a while loop around wait(), not an if statement, because of spurious wake-ups.
  • Prefer notifyAll() over notify() unless you are certain only one thread should wake up.

What should a senior engineer know?

  • Mark word layout: in a 64-bit JVM, the mark word is 8 bytes. It contains the lock state (2 bits), hash code (31 bits), GC age (4 bits), and biased thread ID (54 bits in biased mode).
  • Monitor inflation/deflation: the JVM inflates a lightweight lock to heavyweight under contention and may deflate it back when contention subsides.
  • JIT lock optimizations: lock coarsening merges adjacent synchronized blocks on the same lock. Lock elision removes synchronization on thread-local objects detected by escape analysis.

Visual Overview

DIAGRAM
  Object Header (64-bit JVM)
  ┌─────────────────────────────────────────────┐
  │ Mark Word (8 bytes)                          │
  │  ┌────────┬──────────┬──────┬──────────────┐│
  │  │Lock    │ Hash or  │ GC   │ Thread ID    ││
  │  │State   │ Forwarding│ Age  │ (biased)     ││
  │  │(2 bits)│          │(4bit)│              ││
  │  └────────┴──────────┴──────┴──────────────┘│
  │ Class Pointer (4 or 8 bytes)                 │
  └─────────────────────────────────────────────┘

  Lock Escalation:
  ┌──────────┐   contention   ┌──────────────┐   more    ┌──────────────┐
  │ Biased   │───────────────▶│ Lightweight   │─────────▶│ Heavyweight   │
  │ (free)   │                │ (CAS spin)    │          │ (OS mutex)    │
  └──────────┘                └──────────────┘          └──────────────┘

Key Takeaway

Every Java object has an intrinsic monitor lock stored in the object header. When a thread enters a synchronized block, it acquires that monitor, guaranteeing mutual exclusion (only one thread in the critical section), visibility (writes are flushed to main memory), and effective atomicity (the critical section appears indivisible to other threads).

The JVM does not always pay the full cost of an OS mutex. It starts with biased locking (essentially free for single-thread access), escalates to lightweight locking (a CAS operation for low contention), and only falls back to heavyweight locking (OS mutex with thread parking) under high contention. Understanding this escalation helps explain why synchronization in low-contention code has near-zero overhead.

Remember one sentence: every Java object has a built-in monitor lock, and synchronized acquires it to provide mutual exclusion and memory visibility.

---

Share
Use ← → arrow keys to navigate