Why architecture matters here
The JMM matters because concurrent correctness is impossible to reason about without it, and getting it wrong produces the worst kind of bugs -- rare, non-deterministic, hardware-dependent. Concurrent Java code shares data between threads, and whether one thread correctly sees another's writes is not guaranteed by intuition (the code order) -- it's governed by the JMM's happens-before rules. Without understanding the JMM, developers write code that appears correct (the writes are there in the code) but is subtly broken (the writes may be invisible or reordered) -- producing bugs that are rare (they depend on timing, hardware, JIT optimization -- often not manifesting in testing), non-deterministic (appearing sporadically), and hard to reproduce/debug (they depend on the exact timing and hardware). These are among the hardest bugs in software (concurrency bugs from memory-model violations). The JMM provides the rules to reason about concurrent correctness (using happens-before to ensure visibility and ordering) -- so understanding it is essential to writing correct concurrent code. For anyone writing concurrent Java (shared mutable state across threads), the JMM is the foundation of correctness, and ignorance of it (writing concurrent code without understanding happens-before) is how the worst concurrency bugs arise.
The happens-before insight is the architectural core, and it's the tool for reasoning about concurrent visibility. The JMM doesn't guarantee that writes are immediately visible across threads (that would require expensive constant synchronization -- defeating the performance benefits of caches and reordering). Instead, it guarantees visibility only where there's a happens-before relationship: if A happens-before B (via one of the happens-before rules -- a volatile write-read, a monitor unlock-lock, thread start/join, etc.), then A's effects (including all its writes) are visible to B and ordered before B. Where there's no happens-before relationship between two threads' actions, there's no visibility guarantee (one thread may not see the other's writes -- they can appear stale, reordered, or in impossible combinations). So the way to ensure a thread sees another's writes is to establish a happens-before relationship (via synchronization -- volatile, synchronized, etc.) between the writing and the reading. This is the fundamental tool: to make concurrent code correct, establish happens-before between the write and the read (so the write is visible and ordered) -- and to reason about whether concurrent code is correct, check whether the necessary happens-before relationships exist. Understanding that visibility is governed by happens-before (established by synchronization) -- not by code order or intuition -- is understanding how to reason about and ensure concurrent correctness in Java.
And the data-race-is-undefined-behavior reality is the crucial correctness principle, and it's why the JMM must be respected. A data race is when two threads access the same shared variable concurrently, at least one access is a write, and there's no happens-before ordering between them. The JMM's rule is stark: a program with data races has undefined behavior for those races -- the program may see stale values (an old value, not the latest write), torn reads (a partially-updated value -- e.g., seeing half of a 64-bit write on some platforms), reorderings (operations appearing out of order), or even impossible states (combinations that no sequential execution could produce). This is not 'usually works but occasionally wrong' -- it's undefined (the program's behavior is not guaranteed at all for the races). So data races must be avoided (any shared mutable state accessed by multiple threads must have proper happens-before -- via synchronization -- for every access). This is the crucial principle: data races are undefined behavior (not just 'risky' -- genuinely undefined), so all shared mutable state must be properly synchronized (establishing happens-before). Understanding that data races are undefined behavior -- and thus all shared mutable access must be synchronized -- is understanding why the JMM must be respected rigorously (not 'mostly'), and it's the foundation of correct concurrent Java.
The architecture: every piece explained
Top row: the problem and the mechanisms. The problem: without synchronization, writes may be invisible (cached, not flushed) or reordered (by the CPU or compiler) across threads -- the visibility and ordering problem. Happens-before: the ordering guarantee -- if A happens-before B, A's effects are visible to and ordered before B; established by synchronization. volatile: a volatile field provides visibility and ordering -- a write to a volatile happens-before subsequent reads of it (so readers see the latest write, and the write plus preceding writes are visible/ordered) -- lightweight visibility (no mutual exclusion, just visibility/ordering). synchronized: mutual exclusion plus happens-before -- unlocking a monitor happens-before subsequent locking (so a synchronized block's writes are visible to subsequent synchronized blocks on the same monitor) -- providing both exclusion and visibility.
Middle row: more mechanisms and the hazards. final fields: correctly-constructed final fields are safely published (a final field set in the constructor is guaranteed visible to threads that see the constructed object -- provided the object doesn't escape during construction) -- enabling safe sharing of immutable objects. Atomics and VarHandle: lock-free operations with defined ordering -- AtomicInteger, AtomicReference (atomic operations with happens-before), and VarHandle (fine-grained memory ordering control -- plain, acquire/release, volatile access modes) -- lock-free concurrency with correct ordering. Data races: concurrent shared access without happens-before (at least one a write) -- undefined behavior (stale values, torn reads, reorderings, impossible states) -- to be avoided. Safe publication: correctly sharing an object between threads (so the receiving thread sees the fully-constructed object) -- via happens-before (publishing through a volatile, synchronized, final field, or concurrent collection) -- so shared objects are seen correctly.
Bottom rows: ordering modes and the weakness. Memory ordering modes: different levels of ordering (via VarHandle) -- plain (no ordering -- fastest, for non-shared or already-synchronized), acquire/release (one-way ordering -- release on write, acquire on read, for producer-consumer -- cheaper than volatile), volatile (full ordering) -- the spectrum of ordering strength vs cost (stronger ordering costs more; use the minimum needed). vs sequential consistency: the JMM is weaker than sequential consistency (which would guarantee operations appear in a single global order matching program order) -- the JMM allows reorderings (for performance) that sequential consistency wouldn't, so reasoning must use happens-before (not assume sequential consistency -- the intuition that operations happen in code order across threads is wrong). The ops strip: correctness reasoning (reasoning about concurrent correctness using happens-before -- ensuring the necessary relationships exist for every shared access), review (concurrent code review focusing on the memory model -- checking for data races, correct synchronization -- since these bugs are subtle and hard to test), and testing (concurrency testing -- stress tests, tools like jcstress (the JMM stress-testing tool) -- to surface memory-model bugs, which are hard to catch with normal testing).
End-to-end flow
Trace a visibility bug and its fix. A thread sets a boolean flag (to signal another thread to stop), and the other thread loops checking the flag. Without synchronization (the flag a plain boolean): there's no happens-before relationship between the write (setting the flag) and the read (checking it in the loop) -- so the JMM does NOT guarantee the reading thread sees the write. In practice, the reading thread may never see the flag change (the write cached, not visible; or the loop's read hoisted by the JIT -- caching the flag value, since without synchronization the JIT can assume it doesn't change) -- so the loop runs forever (never seeing the stop signal) -- a classic visibility bug. The fix: make the flag volatile -- now a write to the volatile flag happens-before subsequent reads (so the reading thread sees the write -- the volatile providing visibility and preventing the JIT from caching the read) -- the loop sees the flag change and stops. The volatile established the happens-before relationship (write-before-read visibility) that the plain boolean lacked -- fixing the visibility bug. This illustrates the core: without happens-before (plain field), no visibility guarantee (the write may be invisible); with happens-before (volatile), visibility.
The safe-publication and data-race vignettes show more hazards. A safe-publication case: a thread constructs an object and shares it with another thread. If the sharing is done incorrectly (publishing the object through a plain field, or letting it escape during construction), the receiving thread might see a partially-constructed object (some fields not yet visible -- the construction's writes not yet visible to the receiving thread) -- a safe-publication bug. The fix: publish safely (through a volatile field, synchronized, a final field, or a concurrent collection) -- establishing happens-before so the receiving thread sees the fully-constructed object. A data-race case: two threads increment a shared counter without synchronization (a data race -- concurrent writes without happens-before) -- undefined behavior (lost updates -- increments overwriting each other, since the read-modify-write isn't atomic and there's no ordering). The fix: synchronize (synchronized, or an AtomicInteger -- atomic increment with happens-before) -- eliminating the data race, making the increments correct.
The ordering-mode and testing vignettes complete it. An ordering-mode case: a performance-critical producer-consumer uses acquire/release ordering (via VarHandle -- release on the producer's write, acquire on the consumer's read) instead of full volatile -- cheaper (one-way ordering, sufficient for the producer-consumer handoff) while still correct (the acquire/release establishing the necessary happens-before) -- using the minimum ordering strength needed (acquire/release, not full volatile) for performance. A testing case: the team uses jcstress (the JMM stress-testing tool) to test their concurrent code -- surfacing memory-model bugs (that normal testing misses -- since these bugs are timing/hardware-dependent and rare) by stress-testing the concurrent interactions -- catching the subtle memory-model bugs. The consolidated discipline the team documents: reason about concurrent correctness using happens-before (ensure the necessary happens-before relationships for every shared access -- visibility governed by happens-before, not code order), avoid data races (all shared mutable access properly synchronized -- data races are undefined behavior), use the right mechanisms (volatile for visibility, synchronized for exclusion-plus-visibility, final for safe publication of immutables, atomics/VarHandle for lock-free), publish objects safely, use the minimum ordering strength needed (acquire/release vs volatile), review concurrent code for the memory model, and test with concurrency tools (jcstress) -- because the Java Memory Model governs concurrent visibility and ordering via happens-before, and respecting it rigorously (establishing happens-before for all shared access, avoiding data races) is the foundation of correct concurrent Java, while ignoring it produces the worst kind of bugs (rare, non-deterministic, hardware-dependent).