Question 5 · Section 9

How does synchronized work at monitor level?

synchronized is a keyword in Java that ensures thread safety through locking. Only one thread can execute code inside a synchronized block at a time.

Language versions: English Russian Ukrainian

Junior Level

Basic Understanding

synchronized is a keyword in Java that ensures thread safety through locking. Only one thread can execute code inside a synchronized block at a time.

Mechanism: only one thread can enter a synchronized block at a time. Others wait (parked via the OS). When the first thread exits — one of the waiting threads gets access.

Two Ways to Use

1. synchronized method

public class Counter {
    private int count = 0;

    // Locks the entire method on object 'this'
    public synchronized void increment() {
        count++;
    }
}

2. synchronized block

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

    public void increment() {
        synchronized(lock) { // Locks only this block
            count++;
        }
    }
}

What gets locked?

Type Lock Object
Instance synchronized method this (current object)
static synchronized method ClassName.class (class object)
synchronized(this) block this (current object)
synchronized(lock) block Object lock

Simple Example: Bank Account

public class BankAccount {
    private double balance = 0;

    public synchronized void deposit(double amount) {
        balance += amount; // Only one thread at a time
    }

    public synchronized void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }

    public synchronized double getBalance() {
        return balance;
    }
}

wait() and notify()

public class ProducerConsumer {
    private final List<Integer> queue = new ArrayList<>();
    private final int MAX_SIZE = 10;

    public synchronized void produce(int value) throws InterruptedException {
        while (queue.size() == MAX_SIZE) {
            wait(); // Wait until space is available
        }
        queue.add(value);
        notifyAll(); // Notify consumers
    }

    public synchronized int consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // Wait until data is available
        }
        int value = queue.remove(0);
        notifyAll(); // Notify producers
        return value;
    }
}

Middle Level

Lock State Evolution

The JVM (HotSpot) does not always use heavy locks. There are 4 states:

1. No Lock

Object is not locked. Mark Word (object header) contains:

  • Identity hashcode
  • Age (for GC)

2. Biased Locking — REMOVED in Java 21

If an object is acquired by only one thread, the JVM remembers that thread:

Mark Word: [Thread ID: 54 bits] [Epoch: 2] [Age: 4] [Bias: 1] [01]
  • Thread enters synchronized without CAS operations
  • Just checks: “is this my ID in the header?” → yes → enters
  • Problem: when a second thread appears, the lock must be revoked — expensive

3. Thin Lock (lightweight lock)

Under contention:

Mark Word: [Thread ID: 54 bits] [Lock Record: 6 bits] [00]
  • CAS (Compare-And-Swap) is used for acquisition

    CAS (Compare-And-Swap) — an atomic operation: “write the new value only if current == expected”. Used for thin lock acquisition without OS blocking. Details — in [[9. What is CAS (Compare-And-Swap)]].

  • Thread does not sleep — it spins (loops, waiting)

    Spinning (spin-waiting) — the thread loops in an empty cycle, checking if it can acquire the lock. Faster than parking (does not transfer control to the OS), but consumes CPU.

  • Fast under low contention

4. Fat Lock (heavyweight lock)

Under high contention:

Mark Word: [Object Monitor Pointer: 62 bits] [10]
  • An ObjectMonitor is created in native memory
  • Thread is parked via the OS (futex in Linux)

    Thread parking — the thread “sleeps” and does not consume CPU until the OS wakes it up (when the lock is released). Cheaper than spinning for long waits, but more expensive for short ones (context switch).

  • Transitions to BLOCKED state
  • Expensive (~1000+ ns due to context switch)

Lock Acquisition Algorithm

Thread tries to enter synchronized
         │
         ▼
    Is this a Biased Lock?
    │ yes         │ no
    ▼             ▼
Check         CAS attempt
Thread ID     to acquire
    │             │
    ▼             ▼
My ID?       Success?
│ yes │ no   │ yes  │ no
▼     ▼      ▼      ▼
Enter  Revocation Enter Spin?
               │           │
               ▼           ▼
          Thin Lock   Spin success?
                      │ yes  │ no
                      ▼      ▼
                    Enter   Park (Fat Lock)

Bytecode: monitorenter / monitorexit

synchronized(obj) {
    // critical section
}

Translates to bytecode:

0: aload_1          // Load obj
1: dup              // Duplicate for astore
2: astore_2         // Save for monitorexit
3: monitorenter     // === ACQUIRE MONITOR ===
4: ...              // Critical section
N: aload_2
N+1: monitorexit    // === RELEASE (normal exit) ===
N+2: goto end
N+3: astore_3       // Exception handler
N+4: aload_2
N+5: monitorexit    // === RELEASE (on exception) ===
N+6: aload_3
N+7: athrow
end: return

Important: For synchronized methods, bytecode does NOT contain monitorenter/monitorexit. Instead, the method header has the ACC_SYNCHRONIZED flag, which the JVM handles automatically.

Adaptive Spinning

HotSpot JVM doesn’t just spin in a loop. It remembers history:

  • If spinning succeeded on this monitor last time → spin longer
  • If not → park the thread immediately

This dramatically improves performance for short-lived locks.


Senior Level

Under the Hood: HotSpot Implementation

ObjectMonitor Structure (C++)

// Simplified from OpenJDK
class ObjectMonitor {
public:
    void* _owner;              // Owner thread (Thread*)
    int _Recursions;           // Reentrancy counter
    ObjectWaiter* _EntryList;  // Queue of BLOCKED threads
    ObjectWaiter* _WaitSet;    // Queue of WAITING threads
    jint _WaitSetLock;         // WaitSet protection
    int _contentions;          // Contention counter
    // ...
};

Lock Record on Thread Stack

With Thin Locking, the JVM creates a Lock Record on the stack:

Stack Frame:
┌──────────────────────────┐
│  Local variables          │
├──────────────────────────┤
│  Lock Record:             │
│    - Displaced Mark Word  │  ← Copy of original Mark Word
│    - Owner reference      │  ← Reference to object
├──────────────────────────┤
│  Frame pointer            │
├──────────────────────────┤
│  Return address           │
└──────────────────────────┘

JIT Compiler Optimizations

Lock Coarsening

// Source code:
synchronized(lock) { sb.append("a"); }
synchronized(lock) { sb.append("b"); }
synchronized(lock) { sb.append("c"); }

// JIT merges into one lock:
synchronized(lock) {
    sb.append("a");
    sb.append("b");
    sb.append("c");
}

This reduces the overhead of monitor acquire/release.

Lock Elision (via Escape Analysis)

public String buildString() {
    Object lock = new Object(); // Object doesn't escape the method
    synchronized(lock) {
        // JIT understands: no one else can get lock
        // → completely removes this lock from machine code!
        return "result";
    }
}

Escape Analysis determines:

  • Object is not returned from the method
  • Object is not passed to other methods
  • Object is not saved to static fields

Performance and Highload

Benchmark (approximate)

Scenario Time Note
No lock ~1 ns Base operation
Uncontended synchronized ~10-20 ns Thin lock, no contention
Contended synchronized ~1000-5000 ns (depends on JVM, CPU, OS, load — focus on order of magnitude, not exact numbers) Fat lock + context switch
Biased locking (deprecated) ~5 ns Only one thread

Factors Affecting Performance

  1. Contention Level — how many threads compete
  2. Lock Hold Time — how long the lock is held
  3. Lock Frequency — how often it’s acquired
  4. Number of Locks — how many different locks

Recommendations for Highload

// BAD: Global lock
private static final Object globalLock = new Object();
public void process(Request r) {
    synchronized(globalLock) { // All requests sequential!
        handle(r);
    }
}

// GOOD: Segmented locks
private final Object[] locks = new Object[16];
public void process(Request r) {
    int segment = r.userId() % 16;
    synchronized(locks[segment]) { // Parallelism by segment
        handle(r);
    }
}

// BETTER: Lock-free
private final ConcurrentHashMap<Integer, Data> map = new ConcurrentHashMap<>();
public void process(Request r) {
    map.compute(r.userId(), (k, v) -> handle(r, v)); // Lock-free
}

Diagnostics

Java Flight Recorder (JFR)

java -XX:StartFlightRecording=filename=recording.jfr MyApp

Events:

  • jdk.JavaMonitorEnter — lock wait time
  • jdk.ThreadPark — time in park()

-XX:+PrintFlagsFinal

java -XX:+PrintFlagsFinal -version | grep -i lock

Key flags:

  • UseBiasedLocking (removed in Java 21)
  • PreBlockSpin / SpinBackoffMultiplier

jstack

jstack <pid>

Look for:

"thread-1" #10 BLOCKED (on object monitor)
  - waiting to lock <0x000000076af0c8d0>
  - locked <0x000000076af0c8e0>

Best Practices

  1. Minimize synchronized blocks — only the critical section
  2. Use private final Object lock — encapsulation
  3. Avoid class-level synchronizedsynchronized(ClassName.class)
  4. Consider Lock Coarsening — don’t fragment locks unnecessarily
  5. For Highload — use ConcurrentHashMap, LongAdder, Atomic*
  6. Monitor contention via JFR — growing time = problem
  7. For Java < 15: disable biased locking in high-load systems (-XX:-UseBiasedLocking). For Java 15+, it’s already disabled by default.

Interview Cheat Sheet

Must know:

  • 4 lock states: No Lock → Biased (removed in Java 21) → Thin Lock (CAS + spinning) → Fat Lock (ObjectMonitor)
  • Bytecode of synchronized block: monitorenter/monitorexit; synchronized method: ACC_SYNCHRONIZED flag
  • Two monitorexit generated — for normal exit and for exception (hidden finally)
  • Adaptive Spinning: JVM remembers history — if spinning succeeded, spins longer
  • Lock Coarsening: JIT merges small locks on the same object into one big lock
  • Lock Elision: JIT removes the lock if Escape Analysis shows the object doesn’t “escape”
  • Fat Lock = context switch via OS (futex in Linux), ~1000-5000 ns

Frequent follow-up questions:

  • Why doesn’t a synchronized method contain monitorenter? — JVM handles the ACC_SYNCHRONIZED flag automatically on method call
  • What is a Lock Record? — A structure on the thread’s stack with a copy of the Mark Word; used in Thin Lock without creating ObjectMonitor
  • How does the JVM decide whether to park or spin? — Adaptive Spinning: looks at successful spin history on this monitor
  • When will JIT remove synchronized? — When Escape Analysis shows NoEscape: object is not returned, not passed, not stored

Red flags (do NOT say):

  • “Synchronized always creates ObjectMonitor” — no, only under high contention (Fat Lock)
  • “Biased Locking is enabled in modern Java” — no, removed in Java 21
  • “One monitorexit per synchronized block” — no, two: for normal exit and for exception
  • “Lock Elision works for synchronized(this)” — rarely, since this almost always “escapes”

Related topics:

  • [[4. What is monitor in Java]] — monitor structure and ObjectMonitor
  • [[6. What is the difference between synchronized method and synchronized block]] — ACC_SYNCHRONIZED vs monitorenter
  • [[7. What is reentrant lock]] — monitor reentrancy
  • [[9. What is CAS (Compare-And-Swap)]] — CAS used for Thin Lock acquisition