The idea: a lock is two bits in the object header
Any Java object can be a lock: synchronized (lock) { ... } compiles to monitorenter and monitorexit. HotSpot does not give every object a full mutex. Instead, the first 64 bits of every object, the mark word, end in a 2-bit lock tag. The tag says which of three states the lock is in, and the JVM moves between them only when it has to.
| Tag | State | Who knows the owner |
|---|---|---|
01 | unlocked | nobody holds it |
00 | fast-locked (thin, lightweight) | the owner's lock stack |
10 | inflated | the ObjectMonitor's _owner field |
Thin locking: one CAS
To enter, a thread does one compare-and-swap on the mark word: if the tag is still 01, make it 00. If that works, it pushes the object on its own lock stack, a small per-thread array (8 entries). To exit, it does the reverse CAS and pops. No system call, no extra memory: an uncontended synchronized costs two CAS instructions, a few nanoseconds each. (Demo 1: no contention, two CAS.) See Compare-and-Swap for how a CAS works.
A thread that enters a lock it already holds sees the object on top of its own lock stack, so it just pushes it again. The header is not touched. (Demo 2: recursion on the lock stack.)
Inflation: when a second thread has to wait
If T2's CAS fails because T1 holds the lock, T2 spins for a short while. If T1 is still inside, T2 inflates the lock: the JVM allocates an ObjectMonitor in native memory, records T1 as _owner (and T1's extra lock stack entries as _recursions), and a CAS puts the monitor's address in the mark word with tag 10. T2 spins on the monitor a little more, then adds itself to the entry queue and parks. When T1 exits it must unpark T2, which then takes _owner. (Demo 3: contention inflates, T2 parks; Demo 5: T2 contends with a recursive owner.)
Park and unpark are futex system calls on Linux (see How a Mutex Works: the Futex), and each wait costs two context switches: microseconds instead of nanoseconds. The spinning before parking is a bet that the owner will exit soon; Mutex vs Spinlock shows when that bet pays.
Deflation
Exiting an inflated lock leaves the monitor in place: tag 10, _owner none. The MonitorDeflationThread runs in the background and deflates idle monitors (no owner, no thread queued or waiting): it restores a plain mark word and frees the monitor. After that, the next enter is a thin lock again. (Demo 4: idle monitor deflated, thin again.)
What synchronized costs
| Situation | Work | Rough cost |
|---|---|---|
| no contention | CAS to enter, CAS to exit | nanoseconds |
| re-enter | push / pop, or _recursions ± 1 | almost nothing |
| short contention | spin until the owner exits | CPU time while spinning |
| real contention | inflate once, then park and unpark per wait | microseconds per hand-over |
So synchronized is cheap when threads rarely meet, and the cost of a busy lock is the waiting, not the lock instruction. Inflation also matters for virtual threads: before JDK 24 a virtual thread that blocked inside synchronized pinned its carrier (see Java Virtual Threads vs Platform Threads). What a lock guarantees about visibility is on Java Memory Model, and the entry queue and wait set are drawn on Monitor & Condition Variables.
Biased locking and stack locking
Older JVMs had two other schemes. Biased locking let the first thread "own" an object with no CAS at all, but revoking the bias needed a safepoint; it was disabled in JDK 15 (JEP 374) and removed in JDK 18. Stack locking (the legacy mode until JDK 22) copied the mark word into a lock record on the owner's stack and CAS-ed a pointer to it into the header, so the header was displaced while locked. The lightweight locking shown here (default since JDK 23, -XX:LockingMode=2) keeps the header in place and records ownership on the per-thread lock stack instead.
What the page leaves out
Object.wait(), notify() and the wait set (calling wait() always inflates); identity hash codes; the "anonymous owner" the real inflater sets until the holder claims the monitor; the separate _cxq and _EntryList queues and successor hand-off; adaptive spin counts; lock stack overflow past 8 entries; compact object headers (JEP 450) and the side table they use for monitors; and JIT tricks such as lock elision after escape analysis and lock coarsening.
See where this page fits on the Java & the JVM mind map.