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.
nobody owns the monitor. any thread that enters synchronized (lock) gets it straight away.
the counter, fixed
synchronized on a method locks this for the whole body. here's last chapter's race, done right:
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());
}count = 2000000
the same two threads and the same count++, now wrapped in synchronized (lock).
notice get() is synchronized too. that's not decoration. a lock does two jobs:
- mutual exclusion: only one thread inside at a time, so compound actions can't interleave.
- 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 write | it 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();two different objects, two different monitors. both run at the same time, which is fine: they don't share a counter.
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.
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:
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());
}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:
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 anya 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.
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.
- 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 theClass. - 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.