You write statements in an order. Neither the compiler nor the CPU promises to keep it.
A memory model is the contract that says exactly which rearrangements are allowed — and therefore what one thread may observe of another. It is the specification that happens-before is defined against.
The reorderings
Any two memory operations can in principle be swapped. There are four combinations, and which ones your hardware permits is the model:
- Store → Store
- kept in order
- Load → Load
- kept in order
- Load → Store
- kept in order
- Store → Load
- kept in order
What everyone assumes by default: one global interleaving that all cores agree on. No real CPU does this — it would cost most of the performance of the last thirty years.
The compiler reorders too, and it is worse
Hardware reordering is bounded and local. A compiler can do arbitrary things that are legal under single-threaded semantics:
// You wrote:while (!ready) { }use(data);
// The compiler may emit:if (!ready) { while (true) { } } // ready hoisted into a registerThat is a correct optimisation for a single-threaded program, and it hangs forever in a concurrent one. Reasoning only about the CPU and forgetting the compiler is how people conclude that x86 needs no barriers.
What languages promise
Java (JSR-133, 2004) was the first mainstream language to specify one, and it introduced the guarantee everything else copied: a program with no data races behaves sequentially consistent. Race-free code can be reasoned about simply; the model only matters when you deliberately race.
C++11 made the orderings explicit — relaxed, acquire, release,
acq_rel, seq_cst — and declared a data race undefined behaviour. Not
“unspecified value”: the compiler may assume it cannot happen.
Rust makes the whole question a type-system property. Send and Sync
mean a data race is a compile error rather than a runtime mystery, which is the
single largest practical advance in this area.
Go specifies happens-before for channels, mutexes and sync/atomic, and
declines to define racy behaviour beyond “your program is broken”.
JavaScript has a memory model only for SharedArrayBuffer and Atomics.
Everything else is single-threaded, so the question does not arise.
The orderings, briefly
x.store(1, std::memory_order_relaxed); // atomic, no ordering with anything elsex.store(1, std::memory_order_release); // publishes earlier writesx.load(std::memory_order_acquire); // sees a matching releasex.store(1, std::memory_order_seq_cst); // plus one global total orderRelaxed is atomic and nothing more. Correct for a statistics counter where only the final total matters; wrong for anything that publishes other data.
Acquire/release is the workhorse, and it is what a mutex compiles to.
Sequentially consistent is the default in C++ and Java’s volatile, and it
is the only one strong enough for algorithms needing a single global order —
Dekker’s and Peterson’s mutual exclusion, most notably.