Java Memory Model (JMM): volatile, Memory Barriers & Happens-Before
- Last updated
- Prerequisites:
- Java threads and synchronized basics
- Understanding of CPU caches and memory visibility
- java
- jmm
- volatile
- happens-before
- memory-model
- concurrency
- memory-barriers
Read at your depth
The practical view
The Java Memory Model defines when one thread is guaranteed to see another thread's writes. The key rule: happens-before. If action A happens-before action B, then A's effects are visible to B. Rules that create happens-before: a volatile write happens-before a subsequent volatile read of the same variable; unlock happens-before a subsequent lock of the same monitor; thread.start() happens-before any action in the started thread; thread.join() happens-before everything after it; writing through a thread-safe queue (BlockingQueue) establishes happens-before. volatile gives visibility but not atomicity (use AtomicInteger or synchronized for compound ops).
The same idea in other frameworks
N/A — JVM concurrency concern.N/A — JVM concern.N/A — JVM concern.N/A — JVM concern.Legacy vs modern
Broken visibility with plain fields vs volatile flag
A plain boolean flag read by another thread has no visibility guarantee (may loop forever); volatile publishes the write via happens-before.
static boolean stop = false; // ❌ OLD: not volatile — worker may never see the writestatic volatile boolean stop = false; // ✅ NEW: write → read creates happens-beforeInterview gotchas
Context
This tests the core JMM semantics — interviewers want the visibility-vs-atomicity distinction and the precise meaning of the happens-before edge, not just 'volatile is for flags'.
The mechanical answer
Happens-before is a partial order on actions: if action A happens-before action B, then A's writes are visible to B, and B observes A's ordering. It is NOT a statement about wall-clock time — it's a visibility/ordering contract that lets you reason about concurrent code. volatile creates happens-before between a write and a subsequent read of that variable; synchronized creates it between unlock and subsequent lock; thread start/join and thread-safe queue operations create it too. But volatile does not provide atomicity: count++ is read-modify-write, and two threads doing it concurrently can both read the same value and lose an increment. For atomic compound operations you need synchronized, atomic classes (AtomicInteger using CAS), or locks — which create their own happens-before edges on top of atomicity.
Trap
The naive answer is 'volatile makes the variable thread-safe'. It guarantees visibility/ordering, not atomicity — volatile count++ is still racy. Another trap: thinking happens-before is transitive only within one synchronization chain — it is transitive, which is how the model composes (A→B and B→C implies A→C). Candidates who separate visibility from atomicity show real JMM understanding.
Context
This probes the hardware mapping — interviewers want the barrier types and the architectural difference (x86 TSO vs ARM/POWER weak memory) that explains why volatile costs vary.
The mechanical answer
The JIT compiler emits CPU memory barrier instructions at synchronization points to implement the JMM. The barrier types are LoadLoad, StoreStore, LoadStore, and StoreLoad — each ordering specific pairs of memory operations. On x86 (a TSO — total store order — architecture), the CPU already orders most operations, so volatile reads are nearly free and only a StoreLoad barrier (e.g. mfence or a locked instruction) is needed after a volatile store to prevent store-load reordering. On weak-memory architectures like ARM and POWER, volatile maps to acquire/release semantics using dmb/dsb barriers on both loads and stores, which are more expensive. This is why a volatile-heavy codebase runs differently on different CPUs, and why lock-free code must be written against the JMM (portable) rather than a specific CPU's behavior.
Trap
The naive answer is 'volatile flushes the CPU cache'. The model is about ordering and visibility guarantees implemented via barriers, not literal cache flushes (caches are coherent; the issue is store-buffer/load ordering). Another trap: writing lock-free code that happens to work on x86 and assuming it's correct everywhere — weak-memory CPUs expose missing barriers. Candidates who name the barrier types and the x86-vs-ARM asymmetry show real depth.