skip to content
everything.concurrency.synchronized

synchronized and monitors

every object has a lock. entry sets, reentrancy, and what it guards.

chapter
chapter 09.03
reading time
8 min read

every java object carries a monitor: a lock that one thread at a time can own. synchronized (obj) { ... } grabs obj's monitor, runs the block, and lets go. other threads that want the same monitor wait in line (BLOCKED). that turns read-add-write into one indivisible step, and makes the owner's writes visible to the next owner.

the picture

a single-occupancy room with one chair (the monitor). the door opens from inside only. whoever's inside can stay as long as they like, and can even walk through inner doors of the same room without knocking again (reentrancy). everyone else queues outside the door (the entry set). there's a second waiting area too, for people who stepped out to wait for something specific (the wait set). that one belongs to the next chapter.

outsideRUNNABLE
T1T2T3
entry setBLOCKED
monitor of lockowner
free
T1
T2
T3

nobody owns the monitor. any thread that enters synchronized (lock) gets it straight away.

be the scheduler again. enter with T1, then try entering with T2 and T3: they pile up in the entry set, BLOCKED. let T1 enter again (hold count 2) and exit once: still the owner. the monitor only moves on when the hold count hits 0.

the counter, fixed

synchronized on a method locks this for the whole body. here's last chapter's race, done right:

Main.java
class Counter {
    private int count;
    synchronized void increment() { count++; }
    synchronized int get() { return count; }
}
 
void main() throws InterruptedException {
    Counter c = new Counter();
    Runnable work = () -> {
        for (int i = 0; i < 1_000_000; i++) c.increment();
    };
    Thread a = new Thread(work);
    Thread b = new Thread(work);
    a.start();
    b.start();
    a.join();
    b.join();
    IO.println("count = " + c.get());
}
output
count = 2000000
T1
T2
sharedcount
owner
·
0·

the same two threads and the same count++, now wrapped in synchronized (lock).

step 1/11
the same count++ as the race chapter, inside a monitor. T2's lane is dashed while it's BLOCKED: it doesn't get to do part of its increment early, it doesn't get to do anything.

notice get() is synchronized too. that's not decoration. a lock does two jobs:

  1. mutual exclusion: only one thread inside at a time, so compound actions can't interleave.
  2. visibility: everything a thread wrote before releasing a monitor is visible to the next thread that acquires the same monitor. an unsynchronized get() gets neither, and is allowed to return a stale value forever. the rules behind that are the java memory model.

what actually gets locked

a synchronized method is just shorthand. the two forms on each row below are identical:

you writeit locks
synchronized void inc()synchronized (this) { ... }
static synchronized void reset()synchronized (Counter.class) { ... }
synchronized (lock) { ... }whatever object lock points at right now

so the question "are these thread-safe together?" is always "do they lock the same object?". pick a call for each thread:

class Counter {
    private int n;
    synchronized void inc()          { n++; }      // locks this
    static synchronized void reset() { ... }       // locks Counter.class
    int get()                        { return n; } // locks nothing
}
Counter a = new Counter(), b = new Counter();
T1 calls =
T2 calls =
monitor ofa
T1inside
monitor ofb
T2inside
monitor ofCounter.class
no lock at allget()

two different objects, two different monitors. both run at the same time, which is fine: they don't share a counter.

two calls only exclude each other when they need the same monitor. try a.inc() with a.inc(), then with Counter.reset(), then with a.get(), the unlocked one.

lock objects are per-object, not per-class and not per-method. two threads in two different synchronized methods of the same object still exclude each other (same this). two threads in the same method of two different objects don't.

quick check
T1 calls a.inc() while T2 calls Counter.reset(), a static synchronized method of the same class. what happens?

reentrancy

a thread that owns a monitor can enter it again. the monitor keeps a hold count, and only the last exit releases it. without that, this would deadlock against itself:

Main.java
class Account {
    private int balance = 100;
 
    synchronized void withdraw(int amount) {
        if (canWithdraw(amount)) {   // another synchronized method, same monitor
            balance -= amount;
        }
    }
    synchronized boolean canWithdraw(int amount) { return balance >= amount; }
    synchronized int balance() { return balance; }
}
 
void main() {
    Account acc = new Account();
    acc.withdraw(30);
    IO.println(acc.balance());
}
output
70

withdraw owns acc's monitor, calls canWithdraw, which wants the same monitor, and just walks in. recursion and overriding methods that call super.method() work the same way. this check-and-act is also the fix for last chapter's overdraft: both halves happen under one lock.

what the JVM does with it

a synchronized block compiles to two bytecodes, monitorenter and monitorexit, plus a hidden exception handler that releases the monitor if the block throws. you can never forget to unlock:

javap -c Counter (synchronized (lock) { count++; })
 6: monitorenter
 7: aload_0
 8: dup
 9: getfield      #13    // Field count:I
12: iconst_1
13: iadd
14: putfield      #13    // Field count:I
17: aload_1
18: monitorexit          ← normal exit
19: goto          27
22: astore_2
23: aload_1
24: monitorexit          ← exit when an exception is thrown
25: aload_2
26: athrow
Exception table:
   from    to  target type
      7    19    22   any

a synchronized method doesn't use those at all: it just has the ACC_SYNCHRONIZED flag, and the JVM does the same thing around the call.

one level deeper:where the lock actually lives+

an object doesn't carry a full lock around. its header has a mark word, and when a lock is uncontended the JVM can lock it with a single CAS and a little per-thread bookkeeping, no OS involvement at all. only when threads actually contend (or someone calls wait()) does it inflate into a real ObjectMonitor with an entry list and a wait set, the structure in the figure above. that's why uncontended synchronized is cheap and contended synchronized is not (threads get parked by the OS). biased locking, an older trick for the single-thread case, was turned off in java 15 and later removed. the details are in how locks work inside.

the gotchas

synchronized can't time out, can't be interrupted while waiting, and can't be tried without blocking. when you need any of those, that's what ReentrantLock is for.

keylines
  • every object has a monitor. synchronized owns it for the length of the block.
  • it gives mutual exclusion and visibility, and you need both, so reads get locked too.
  • two calls only exclude each other if they lock the same object.
  • synchronized methods lock this, static ones lock the Class.
  • reentrant: the owner can re-enter, and only the last exit releases.
  • lock a private final object. never a boxed number, a string literal, or a field that changes.