Why does even an empty synchronized block carry memory-barrier (acquire/release) effects, and what does this reveal about where Java's visibility guarantee comes from?
answer
- barrier attaches to acquire/release events, not the body
- acquire = invalidate + re-read; release = flush prior writes
- empty block = acquire; release with nothing between, still fences
- visibility is a property of the protocol, not the critical section
- modern code: use volatile / VarHandle fences instead
basics
~20 sBecause the visibility comes from taking and releasing the lock, not from the code inside. Entering the block forces a fresh read of shared memory and exiting forces your changes out to memory, so an empty synchronized block still publishes and refreshes data.
solid answer
~50 sThe acquire and release barriers are attached to the monitor operations themselves, not to the statements in the block. Entering performs a monitor acquire (an acquire barrier: invalidate caches, re-read shared state, no later read may move before it). Exiting performs a monitor release (a release barrier: flush all prior writes, no earlier write may move after it). Since these barriers fire at the boundary, an empty `synchronized (lock) {}` still flushes everything the thread wrote before it and, if it had previously acquired and read, refreshes its view. This reveals the core truth: Java's cross-thread visibility is produced by crossing the monitor boundary and establishing the monitor-lock happens-before edge, not by doing work under the lock. In practice you would not write empty blocks (use volatile or a real critical section), but understanding it explains why the protocol — not the body — is what makes shared state visible.
go deeper
May not yet know this nuance; enough to recognize that synchronized affects visibility and that locking, not the code inside, is what matters for sharing data.
Can state that entering re-reads and exiting flushes, and infer that an empty block still does both because the effect is on the lock operations.
Explains barriers are bound to acquire/release events, articulates 'visibility is a property of the protocol not the body,' and knows it's a conceptual probe rather than a coding pattern.
Ties it to JSR-133 semantics, volatile/VarHandle fences, lock elision/biased-locking history, and can advise teams on correct fencing idioms and why empty-block fencing is an anti-pattern.
## Setting the stage: barriers vs. block bodies To see why an empty `synchronized` block still 'does something', you must understand that the memory guarantees attach to two **events** — acquiring a monitor and releasing it — and not to the statements between them. ### Define the events - **Monitor acquire** happens on entry to a `synchronized` region. The JVM marks the object's lock as held by this thread (blocking if another thread holds it). - **Monitor release** happens on exit (normal or exceptional). The JVM frees the lock. ### Define the barriers A **memory barrier** (a.k.a. fence) is an instruction/semantic that restricts how reads and writes may be reordered and when they become visible: - An **acquire barrier** says: operations *after* it cannot be moved to *before* it, and the thread refreshes its view of shared memory. Practically, caches are invalidated so the thread re-reads current values rather than trusting stale local copies. - A **release barrier** says: operations *before* it cannot be moved to *after* it, and all those prior writes are made visible (flushed) before the barrier completes. The JMM binds an **acquire barrier to monitor acquire** and a **release barrier to monitor release**. That binding is the whole point: it is the *act of acquiring/releasing*, independent of any body, that carries the effect. ### Therefore: the empty block Consider: ```java synchronized (lock) { } ``` This is `acquire; release` with nothing in between. The acquire still invalidates and re-reads; the release still flushes prior writes. So if thread A had earlier mutated shared fields (anywhere before this point, by program order) and then runs this empty block, the release publishes those writes. A thread B that subsequently does its own `synchronized (lock) { }` performs an acquire and is guaranteed (by the **monitor-lock happens-before rule**: an unlock happens-before every later lock of the same monitor) to observe A's published writes. ### What this reveals It reveals that **visibility in Java is a property of the synchronization protocol, not of the work done inside the critical section.** People sometimes imagine the lock 'protects the variables you touch inside.' More precisely, the lock establishes ordering edges; what becomes visible is *everything before the release*, surfacing to *everything after a matching acquire*. The body is where you *use* mutual exclusion; the boundary is where you *get* the memory effect. ### Historical / idiom note Before `volatile` had today's strong semantics (Java 5+, JSR-133), and in some low-level coordination code, people occasionally used empty or near-empty synchronized blocks purely as a fence. Today that is an anti-pattern: prefer `volatile`, `final`, `java.util.concurrent` primitives, or `VarHandle` fences (`fullFence`, `acquireFence`, `releaseFence`) when you genuinely need an explicit barrier. The empty-block fact is best treated as a *conceptual* probe of where the guarantee lives, not a coding recommendation. ### Relationship to volatile `volatile` gives a similar acquire (on read) / release (on write) pairing without mutual exclusion. A volatile write is like a lightweight release; a volatile read is like a lightweight acquire. So the same 'boundary, not body' principle underlies volatile too — there is no body at all, just the single field access carrying the barrier. ### Caveat: don't conclude empty blocks are useful The JIT can, in principle, optimize a provably-uncontended, effect-free lock (biased/elided locking historically), but the *memory-model semantics* still require the ordering be respected as observed. Regardless, relying on empty blocks for fencing is fragile and unclear; the takeaway is understanding, not a technique.
- If empty synchronized blocks carry barriers, what is the modern, clearer way to express an explicit memory fence in Java?Use volatile fields for the common publish/observe pattern, or VarHandle.fullFence()/acquireFence()/releaseFence() (or the legacy Unsafe fences) when you need a standalone fence without a lock. These state intent directly instead of abusing an empty critical section.
- How does a volatile field relate to the 'boundary, not body' idea?A volatile write behaves like a release barrier and a volatile read like an acquire barrier, with no block at all — the single field access carries the barrier. It's the same principle: the guarantee rides on the synchronization action, not on surrounding work.
saying these in an interview costs you the question
- Claiming an empty synchronized block is a no-op the JVM fully removes for memory purposes
- Saying the lock only protects variables written inside the block
- Recommending empty synchronized blocks as the right way to fence in modern code
- Confusing the acquire effect (entry) with the release effect (exit)