Overview
The Java Memory Model (JSR-133, JLS §17.4) is not about heap layout — it is the contract defining which writes a thread is guaranteed to see from other threads, given that compilers, JITs and CPUs all reorder aggressively. Its central bargain: write data-race-free programs and the JVM promises sequential consistency — execution behaves as if operations interleave in some global order. Race, and you get weakly defined behaviour: stale reads, half-published objects, impossible-looking states. The JMM is what makes concurrent Java analyzable rather than empirical.
Key points
- Happens-before is the whole game: a write is visible to a read iff a happens-before (HB) chain connects them. HB is transitive; no chain, no guarantee — no matter how much wall-clock time passed.
- The HB sources to memorize: program order within a thread · monitor unlock → later
lock of the same monitor ·
volatilewrite → later read of the same variable ·Thread.start()→ everything in the started thread · everything in a thread →join()on it returning ·Future.get(), and the documented HB edges ofjava.util.concurrent(queue put → take, executor submit → task run). volatile= visibility + ordering, never atomicity: a volatile write publishes everything written before it to whoever reads the flag afterwards (piggybacking); butcount++on a volatile is still a lost-update race — that needsAtomicInteger/LongAdderor a lock.finalfields have their own rule: fields assigned in the constructor (withthisnot escaping) are visible fully initialized to every thread, even without synchronization — the guarantee immutable objects stand on, and whyStringcan be shared freely.- Safe publication idioms:
volatile/AtomicReferencefield · lock-guarded field ·static final(class-init guarantee) · final field of a properly constructed object · handing off through a concurrent collection. Plain field assignment is not one — that's the double-checked-locking bug (fixed only by making the fieldvolatile). - Stronger tools exist for weaker orderings:
VarHandle(Java 9+) exposes acquire/release and opaque modes below volatile's full seq-cst — mechanical-sympathy territory; default tovolatileandjava.util.concurrentuntil profiling says otherwise. - Roads not taken: 100% single-threaded code needs none of this; but "it worked in the test" is the JMM's trap — racy code often runs correctly for months on x86 (strong hardware ordering) and breaks on ARM or after a JIT upgrade.
Examples
class Publisher {
private int payload; // plain field
private volatile boolean ready; // the flag carries the HB edge
void publish() { payload = 42; ready = true; } // write payload BEFORE flag
int consume() { return ready ? payload : -1; } // reading true => sees 42
}
// Without volatile on `ready`, consume() may see ready==true and payload==0:
// both reorderings (write side and read side) are legal in a racy program.
The happens-before chain in that example, drawn out — transitivity is what connects the payload write to the payload read:
flowchart LR
subgraph writer ["Writer thread"]
W1["payload = 42"] -->|program order| W2["volatile write: ready = true"]
end
subgraph reader ["Reader thread"]
R1["volatile read: ready == true"] -->|program order| R2["read payload: sees 42"]
end
W2 -->|volatile write happens-before later read| R1
Related
- Java memory & GC — the map — why "memory model" ≠ memory management.
- JVM memory anatomy — the physical layout this ordering contract sits above.
- Concurrency and Parallelism — the cross-language concurrency crossroads; every language with shared memory needs a memory model.
- Deep dive Java — virtual threads and
java.util.concurrentas the applied layer over these guarantees. - Deep dive Go and Deep dive Rust — Go's happens-before doc and Rust's ownership system: two other answers to the same visibility problem.
Citations
[1] JLS §17.4 — Memory Model [2] JSR-133 FAQ (Manson & Goetz) [3] Aleksey Shipilëv — Java Memory Model Pragmatics