Why synchronized is slower – tracing its roots from MESI cache coherence
The article demystifies why Java's synchronized keyword can be slower by tracing the full path from CPU cache‑coherence (MESI) through bus and cache locks, volatile, biased and lightweight locks, up to the heavyweight monitor implementation, revealing each layer’s impact on visibility, ordering and atomicity.
Why synchronized can be slower?
Many claim the slowdown is due to its heavyweight nature and kernel‑mode transitions, but the real story starts deeper—in the CPU cache and the MESI coherence protocol.
1. The starting point: CPU lies
Cache hierarchy speeds and capacities:
L1 Cache – ~1 ns, 32 KB
L2 Cache – ~3 ns, 256 KB
L3 Cache – ~10 ns, 12 MB
Main Memory – ~100 ns, GB‑scale
The CPU‑to‑memory speed gap can be up to 100×, so architects added three cache levels to avoid CPU stalls. This is the principle of locality (temporal and spatial).
2. Cache‑coherence disaster: two threads, two copies
Thread A on core 1 changes i from 1 to 2, writing the value to its L1 cache.
Thread B on core 2 reads i from its own L1 cache and still sees 1.Because each core has a private cache, the modification is invisible to the other core—this is the cache‑coherence problem.
3. MESI protocol – the CPU’s group chat
Intel’s solution is the MESI (Modified, Exclusive, Shared, Invalid) protocol, an invalidate‑based coherence scheme.
M (Modified) – modified, only this core has the line, out of sync with memory.
E (Exclusive) – exclusive, only this core has the line, in sync with memory.
S (Shared) – shared, multiple cores have the line, in sync with memory.
I (Invalid) – invalid, the line is stale and must be re‑fetched.
When a core writes a shared line, it broadcasts a bus‑snoop signal that marks the same line in other cores as I . This broadcast is called bus snooping .
Core 1 emits a snoop: “I’m modifying i”.
Core 2 receives it and marks its copy of the line as I .
When Core 2 later reads i, it sees the I state and must fetch the latest value from main memory.
MESI thus provides the visibility guarantee at the CPU level.
4. From bus lock to cache lock – two “aggressive” schemes
If MESI alone isn’t enough for an atomic operation like i++ (read‑modify‑write), the CPU offers two lock mechanisms.
Option 1: Bus Lock – simple but brutal
The processor asserts the LOCK# signal on the bus, forcing all other cores to stop accessing memory.
Pros: absolute safety, strong atomicity
Cons: huge granularity, blocks all threads, CPU spins, very low performance
Status: rarely used todayOption 2: Cache Lock – precise targeting
Only the cache line containing the variable is locked.
The current core writes back its cache line to main memory.
Other cores’ corresponding cache lines are marked I .
Pros: fine‑grained, high performance
Cons: degrades to bus lock if data spans many lines
Status: the mainstream technique behind Java’s lightweight synchronizationVolatile’s implementation follows this path: a memory barrier plus a LOCK prefix.
5. volatile – a lightweight lock using LOCK
On x86, volatile adds a memory barrier (StoreStore, StoreLoad, LoadLoad+LoadStore) and a LOCK prefix. It guarantees visibility and ordering but not atomicity.
i++ is three steps: read → modify → write. volatile only ensures the read and write are visible; the modify step still occurs in the local cache, leading to race conditions.
Thus volatile is a non‑blocking, non‑locking synchronization primitive with limited capabilities.
6. synchronized – the full chain from bytecode to monitor
The JVM does not recognize synchronized directly; it is syntactic sugar compiled to bytecode.
Sync block → monitorenter + monitorexit Sync method → ACC_SYNCHRONIZED flag
6.1 The heavyweight lock: Monitor
Every Java object has an associated Monitor (implemented by the C++ ObjectMonitor structure) containing:
_owner → thread holding the lock
_recursions → re‑entry count
_EntryList → queue of threads waiting for the lock
_WaitSet → queue of threads waiting via wait()Lock acquisition flow:
Thread executes monitorenter and tries to obtain the object's Monitor.
If successful, _recursions increments and the thread becomes the owner.
Other threads reaching monitorenter see the Monitor occupied and are placed into _EntryList (blocked).
When the owning thread executes monitorexit, _recursions decrements; if it reaches zero, the Monitor is released and waiting threads are awakened.
The Monitor relies on the OS’s mutex lock, involving a user‑mode ↔ kernel‑mode transition—hence the “heavyweight” cost.
Since this cost is high, the JVM introduced a lock‑upgrade mechanism (JDK 1.6) that escalates from no‑lock to heavyweight only when needed.
7. Lock upgrade ladder
Upgrade path: no‑lock → biased lock → lightweight lock → heavyweight lock (never downgraded).
All lock states are recorded in the object header’s Mark Word.
┌──────────────────────────────────────────┐
│ Java object header │
├──────────────────┬───────────────────────┤
│ Mark Word │ Klass Word (class ptr)│
│ (lock state / │ │
│ thread ID / hash│ │
│ age) │ │
└──────────────────┴───────────────────────┘Level 1: No‑lock
New objects have only hash code, GC age, and no lock bits. All threads can access freely—zero overhead.
Level 2: Biased lock – “it’s my lock”
Assumes most locks are uncontended and repeatedly acquired by the same thread.
First acquisition writes the thread ID into the Mark Word.
Subsequent acquisitions by the same thread see its own ID and skip CAS, proceeding directly.
If another thread attempts the lock, a CAS competition occurs; on failure the biased lock is revoked, which requires a global safepoint and thread pause.
⚠️ Important update: Biased locking was deprecated in JDK 15 (JEP 374) and effectively removed after JDK 18 because its revocation cost outweighs its benefits in modern high‑concurrency workloads.
Level 3: Lightweight lock – “spin, don’t sleep”
When contention appears, the JVM creates a Lock Record on the thread’s stack, copies the object's Mark Word, and attempts a CAS to replace the Mark Word with a pointer to the Lock Record.
CAS success → lightweight lock acquired; thread executes synchronized code.
CAS failure → another thread is competing; the thread spins (default 10 times) before falling back.
Release flow:
CAS writes the displaced Mark Word back to the object header.
If successful, the lock is released.
If another thread is still competing, the lock inflates to a heavyweight lock.
JDK 1.6 added adaptive spinning: successful spins increase the spin count next time; failed spins decrease it, possibly skipping spinning altogether.
If spinning exceeds a threshold (or the number of spinning threads exceeds half the CPU cores), the lightweight lock inflates to a heavyweight lock.
Level 4: Heavyweight lock – block and wait
Under heavy contention, the Mark Word is replaced with a pointer to an OS mutex. Threads that fail to acquire the lock block (state BLOCKED) and are placed into _EntryList. When the lock is released, one waiting thread is awakened.
Acquisition failure → thread enters BLOCKED state
↓
Enters _EntryList waiting queue
↓
On lock release, one thread is woken upThis involves a user‑mode ↔ kernel‑mode context switch, the highest overhead but also the most fair.
8. A single diagram that ties everything together
┌─────────────────────────────────────────┐
│ Multithreaded access to shared i │
└──────────────────┬──────────────────────┘
▼
┌─────────────────────┐
│ CPU core private caches (L1/L2/L3) │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ MESI protocol snooping │
│ modify → broadcast → invalidate │
└──────────┬──────────┘
▼
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Bus lock │ │ Cache lock│ │ volatile │
│ LOCK# │ │ LOCK prefix│ │ memory barrier│
└──────────┘ └──────────┘ └──────────────┘
│
▼
┌──────────────────┐
│ synchronized │
│ monitorenter/exit│
└────────┬─────────┘
▼
┌───────────────────┐
│ lock escalation │
│ (no‑lock → biased → lightweight → heavyweight) │
└───────────────────┘9. Final thoughts
Many know that synchronized provides atomicity, visibility, and ordering, but without understanding MESI, memory barriers, monitors, and lock escalation you only memorize the answer, not the reasoning.
MESI solves visibility.
Memory barriers solve ordering.
Monitor + OS mutex solves atomicity.
Lock escalation solves performance by minimizing cost.
These concepts form a continuous chain from hardware to software, from physical to logical. The next time someone asks about the internals of synchronized, you can explain the whole path instead of just mentioning “monitor”.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer1970
Formerly called 'Code to 35'. Add our main WeChat ID to access a wealth of shared resources (algorithms, interview prep, tech stacks: Java, Python, Go, big data). We mainly share serious development techniques, focusing on output-driven input. Occasionally we post life snippets and gossip. Our aim is to attract precise traffic and test advertising opportunities.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
