Beyond mutual exclusion, what memory effects does Java's synchronized keyword guarantee when a thread exits and another thread later enters a synchronized block on the same lock?
answer
- synchronized = mutual exclusion + visibility (two jobs)
- release on exit = flush; acquire on entry = invalidate
- monitor-lock rule: unlock happens-before next lock of SAME monitor
- same lock object or no edge
- barrier is on the boundary, not the body (empty block still works)
basics
~20 ssynchronized does two jobs. It blocks other threads from the same locked code (mutual exclusion), and it makes memory visible: when a thread leaves the block its changes are pushed to main memory, and when another thread enters on the same lock it re-reads fresh values instead of stale cached ones.
solid answer
~50 ssynchronized provides two guarantees: mutual exclusion and memory visibility. The visibility part is the key insight. Releasing a monitor (exiting the synchronized block) acts as a release barrier: all writes the thread made become visible to main memory. Acquiring that same monitor (entering a synchronized block on the same lock object) acts as an acquire barrier: the thread invalidates its cached view and re-reads, so it sees every write made before the previous holder released. This pairing creates a happens-before edge: everything before the release happens-before everything after a subsequent acquire of the same lock. Without it, a thread could read stale cached values, see reorderings, or never observe another thread's updates at all. Crucially, the visibility effect holds even if the synchronized block is empty, because the barrier is tied to acquire/release, not to the body.
go deeper
Knows synchronized stops two threads running the same code at once. At this level it's enough to also state that it 'makes changes visible to other threads', even without the barrier vocabulary.
Can name both guarantees (mutual exclusion + visibility) and explain that exiting flushes writes while entering re-reads, and that both threads must use the same lock.
Frames it as acquire/release barriers implementing the monitor-lock happens-before edge; knows writes before the release are published and that an empty block still carries the effect.
Connects it to the full JMM (happens-before transitivity, relation to volatile/final, hardware store buffers and cache coherence), and can reason about subtle publication and reordering bugs and how to design lock protocols that avoid them.
## The problem synchronized solves (two problems, actually) When people first learn `synchronized`, they think it only stops two threads from running the same code at once. That is **mutual exclusion**, and it is real, but it is only half the story. The other half is **memory visibility**, and it is the part interviews probe. ### Why visibility is even a question A modern CPU does not read and write directly to one shared block of main memory for every operation. Each core has its own **caches** (small fast memory) and **registers**. The compiler and the CPU are also free to **reorder** independent instructions for speed. So when thread A on core 1 does `x = 5`, that write may sit in core 1's cache or store buffer for a while. Thread B on core 2 reading `x` might still see the old value from its own cache. There is no automatic, instant propagation of writes between cores. The **Java Memory Model (JMM)** is the contract that says *under what conditions* one thread is guaranteed to see another thread's writes. By default, with no synchronization, the answer is: **no guarantee**. The JVM is allowed to let thread B never see thread A's write, or see writes out of order. ### Monitors and locks Every Java object has an associated **monitor** (also called an intrinsic lock). `synchronized(obj) { ... }` does two things tied to that object's monitor: - On **entry** it performs a **monitor acquire** (it takes the lock; if another thread holds it, this thread blocks). - On **exit** (including via an exception) it performs a **monitor release** (it gives up the lock). `synchronized` methods are the same thing: an instance method locks `this`, a static method locks the `Class` object. ### The memory barriers (the heart of this topic) The JMM attaches **memory-barrier semantics** to acquire and release: - **Release (on exit) = a 'flush' / release barrier.** Before the monitor is released, every write the thread made inside (and before) the block must be made visible to main memory. The runtime is not allowed to keep those writes hidden in a private cache or move them *after* the release. - **Acquire (on entry) = an 'invalidate' / acquire barrier.** When the thread takes the monitor, it must discard (invalidate) its cached assumptions and re-read shared state, so it observes whatever the previous releaser flushed. Reads inside the block cannot be moved *before* the acquire. Together these are an **acquire/release pair**. The net effect: **everything a thread did before releasing a monitor happens-before everything another thread does after acquiring that same monitor.** ### Happens-before — the formal name **Happens-before** is the JMM's ordering relation. If action X happens-before action Y, then X's memory effects are guaranteed visible to Y, and X is ordered before Y. The **monitor-lock rule** states: an unlock on a monitor happens-before every subsequent lock on *that same* monitor. That single rule is what `synchronized`'s memory effects implement. (Other happens-before edges exist too: `volatile` write→read, `Thread.start`, `Thread.join`, final-field publication — but the monitor rule is the one `synchronized` gives you.) ### The 'same lock' requirement The edge only exists between acquire and release of the **same** monitor object. Two threads synchronizing on **different** objects get mutual exclusion against no one and **no** happens-before edge between them. A classic bug is locking on a freshly-`new`ed object, or on a `Integer`/`String` that is not actually shared, so the threads never contend the same monitor. ### The empty-block surprise Because the barriers are bound to acquire/release and not to the block body, even ```java synchronized (lock) { } ``` carries the full release (on the prior thread's exit) and acquire (on this thread's entry) effects. You will not normally write empty blocks, but it demonstrates the principle: the visibility comes from crossing the monitor boundary, not from the work inside. ### Putting it together (the canonical example) ```java int data; // ordinary field final Object lock = new Object(); boolean ready; // Thread A data = 42; synchronized (lock) { ready = true; } // release flushes data and ready // Thread B synchronized (lock) { if (ready) { /* acquire: sees data == 42 */ } } ``` Writes that occurred *before* A released `lock` (including the non-synchronized `data = 42`, by program order it precedes the release) are visible to B after B acquires `lock`. The barrier publishes more than the variables touched inside the block. ### Why this matters in practice If you protect a shared mutable field with a lock on *write* but read it without the lock, you keep the read fast but lose the visibility/ordering guarantee — you can read a stale or torn value. The rule of thumb: **all accesses (reads and writes) to a piece of shared mutable state must be synchronized on the same lock** (or the field made `volatile`/`final` as appropriate). Then the acquire/release pairing guarantees each reader sees the latest write.
- Does the visibility guarantee apply to a variable that is written outside the synchronized block but before the monitor is released?Yes. By program order, any write that precedes the release is part of what gets flushed, and a thread acquiring the same monitor afterward is guaranteed to see it. The barrier publishes all prior writes, not just those textually inside the block.
- If two threads synchronize on different lock objects, do they get any cross-thread visibility guarantee?No. The monitor-lock happens-before edge requires the same monitor. Different locks give mutual exclusion against nobody in common and no happens-before, so one thread may never see the other's writes.
saying these in an interview costs you the question
- Saying synchronized only provides mutual exclusion and ignoring visibility
- Claiming the happens-before edge exists across different lock objects
- Thinking only variables written inside the block are published (writes before the release are too)
- Believing an empty synchronized block has no memory effect