Skip to article
ALGORITHMICSConcurrency
Concurrency6 min read

Memory Models

What the hardware and the compiler are allowed to reorder — and why x86 lets bad code pass.


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:

Memory 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 register

That 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 else
x.store(1, std::memory_order_release); // publishes earlier writes
x.load(std::memory_order_acquire); // sees a matching release
x.store(1, std::memory_order_seq_cst); // plus one global total order

Relaxed 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.