skip to content
everything.concurrency.javaMemoryModel

the java memory model

happens-before, volatile, reordering, and safe publication.

chapter
chapter 09.05
reading time
11 min read

without synchronization, one thread may see another's writes late, never, or in a different order than they were made. the java memory model (JLS §17.4) is the contract that says when a write is guaranteed visible: when it happens-before the read. locks, volatile, start() and join() create those edges. no edge, no guarantee.

the picture

you'd think memory is one big shared whiteboard, and every write lands on it the instant it happens, in program order. that's the model in your head, and it has a name: sequential consistency. no real machine works like that, because it would be painfully slow.

instead, every core keeps its own scribbles: values in registers, writes queued in a store buffer waiting to reach the cache, lines sitting in its private cache. on top of that, the JIT freely reorders, merges and deletes memory accesses when the current thread can't tell the difference. "the current thread can't tell" is the key phrase. other threads can.

the memory model is the deal between you and all that machinery: write your program with proper happens-before edges, and everything behaves as if there were one whiteboard. skip them, and you get whatever the hardware and the JIT felt like.

the litmus test

the classic four-line program that breaks everyone's intuition:

StoreBuffering.java
int x = 0, y = 0;
 
// thread 1          // thread 2
x = 1;               y = 1;
int r1 = y;          int r2 = x;
predict the output

after both threads finish, can r1 == 0 && r2 == 0?

memory =
shared: int x = 0, y = 0;
T1core 1
  1. x = 1;
  2. r1 = y;
r1 =·
T2core 2
  1. y = 1;
  2. r2 = x;
r2 =·
shared memory
x0
y0

x and y start at 0. each thread writes one and reads the other. is r1 = 0 and r2 = 0 possible?

outcomes seen (r1, r2):0, 11, 01, 10, 0
start in what you imagine: try every order, you'll only ever get 0,1 1,0 or 1,1. then switch to store buffers, step both writes first, then both reads. the writes are stuck in the buffers, so both reads see memory's 0. finally try volatile.

this isn't a theoretical curiosity. I ran exactly this test with jcstress (the OpenJDK tool built for testing the memory model) on jdk 25, on an arm mac:

r1, r2frequencysamplesexpect
0, 055.49%252.3Minteresting
0, 121.05%95.7Macceptable
1, 023.46%106.6Macceptable
1, 1<0.01%923acceptable
real results, 454 million runs of the plain version and 165 million of the volatile one. with plain ints, the "impossible" 0, 0 was the single most common outcome. with volatile it never happened once, which is the forbidden line holding.

that's not a broken CPU. ARM and x86 both have store buffers, and both allow this particular reordering (a store followed by a load of a different address). ARM allows a lot more than x86 does. the memory model has to work on all of them, so it promises very little for plain fields and lets you ask for more where you need it.

happens-before

the JMM doesn't talk about caches or store buffers at all. it defines one relation between actions, happens-before, and one promise:

if a write happens-before a read of the same variable (and no other write sneaks in), the read sees that write.

program order
within one thread, each action happens-before the next one. so a single thread always sees its own writes, in order.
monitor
an unlock of a monitor happens-before every later lock of that same monitor.
volatile
a write to a volatile field happens-before every later read of that field.
thread start
t.start() happens-before every action in t.
thread end
every action in t happens-before another thread's t.join() returning (or t.isAlive() returning false).
transitivity
if a happens-before b, and b happens-before c, then a happens-before c. this is where all the power is.

(there are a few more: interrupting a thread happens-before it noticing the interrupt, and the default zero of every field happens-before every thread's first action. the full list is JLS §17.4.4 and §17.4.5.)

T1
T2
data = 42
ready = true
volatile write
while (!ready)
volatile read, sees true
r = data
result:r = 42, guaranteed

a write to a volatile field happens-before every later read of that field. everything T1 did before the write rides along.

click through the rules. grey arrows are program order. the amber arrow is the cross-thread edge the rule adds. when a chain of arrows connects data = 42 to r = data, it turns green and the read is guaranteed. on no edge, there's no chain, so there's no guarantee.

notice that the volatile edge protects data, which isn't volatile at all. T1's write of data happens-before its volatile write (program order), which happens-before T2's volatile read (volatile rule), which happens-before T2's read of data (program order). transitivity does the rest. this is called piggybacking, and it's how almost every concurrent class in the JDK publishes its state: one volatile or one lock, guarding a pile of plain fields.

the jcstress version of that exact message-passing pattern, again on jdk 25:

ready, datafrequencysamplesexpect
0, 056.99%366.2Macceptable
0, 420.03%214.7kacceptable
1, 0<0.01%55.1kinteresting
1, 4242.97%276.1Macceptable
T2 reads ready then data. with a plain flag, T2 saw ready == true but data == 0 55,084 times. rare, but it's the kind of rare that becomes a weekly incident at scale. making only the flag volatile took that to zero.

volatile: visibility, not atomicity

a volatile field gives you two things: every read sees the latest write (in the happens-before sense), and plain reads and writes can't be moved across it in the dangerous directions. it's the cheapest synchronization java has.

here's the stop flag from effective java item 78, without volatile:

StopThread.java
public class StopThread {
    private static boolean stopRequested;
 
    public static void main(String[] args) throws InterruptedException {
        Thread background = new Thread(() -> {
            long i = 0;
            while (!stopRequested) i++;
            System.out.println("stopped after " + i);
        });
        background.start();
        Thread.sleep(1000);
        stopRequested = true;
        System.out.println("main: set stopRequested = true");
    }
}

on jdk 25 on my machine, three runs out of three printed main: set stopRequested = true and then hung forever, the background thread spinning on a core. with -Xint (interpreter only, no JIT) it stopped right away, after about 225 million iterations. so it's not that the write never reaches memory. it's that C2 compiled the loop, saw no happens-before edge that could ever change stopRequested inside it, and read the field once:

java
// what the JIT is allowed to turn the loop into
if (!stopRequested) {
    while (true) i++;
}

completely legal under the JMM. whether it actually hangs depends on the JIT, the timing and the platform, which is exactly the problem: it might pass every test and hang in prod. one keyword fixes it, and now it behaves the same every time:

Main.java
static volatile boolean stopRequested;
 
void main() throws InterruptedException {
    Thread background = new Thread(() -> {
        long i = 0;
        while (!stopRequested) i++;
        IO.println("stopped");
    });
    background.start();
    Thread.sleep(1000);
    stopRequested = true;
    background.join();
    IO.println("done");
}
output
stopped
done
one level deeper:what the JIT actually emits for volatile+

on x86, ordinary loads and stores are already strongly ordered except for the store-then-load case from the litmus test, so a volatile read is a plain load, and a volatile write is followed by a fence (a lock-prefixed instruction) that drains the store buffer. on ARM (aarch64), HotSpot uses the load-acquire and store-release instructions, ldar and stlr, which the hardware guarantees not to reorder with each other. same java semantics, different machine code. you never write any of it: you write volatile, the JIT picks the barriers.

final fields and safe publication

there's one more way to be surprised: seeing an object before it's finished. p = new Point(1, 2) is several steps (allocate, write x, write y, store the reference), and without happens-before, another thread can observe them in a different order.

T1
T2
sharedp
p.x
p.y
·
null··

T1 runs p = new Point(1, 2). T2 polls: if (p != null) use p.x and p.y. Point's fields are plain ints.

step 1/7
a racy publication. T2 sees the reference before the field writes, and reads a half-built object. nothing here throws. it just quietly reads the wrong numbers.

final fields come with a special guarantee (JLS §17.5): when a constructor finishes, its final fields are frozen, and any thread that gets a reference to the object after that sees the final fields' constructed values, even if the reference was passed through a data race. that's what makes immutable objects like String safe to share without any locking. the one way to break it: let this escape from the constructor (register a listener, start a thread, store this somewhere) before it finishes.

for everything that isn't final, you need to publish safely, which just means: hand over the reference through a happens-before edge.

safe publicationwhy it works
initialize in a static initializerclass initialization is done under a lock by the JVM
store into a volatile field or an AtomicReferencevolatile write → read edge
store into a final field of a properly constructed objectthe final-field freeze
store into a field guarded by a lockunlock → lock edge
put it in a concurrent collection (ConcurrentHashMap, a BlockingQueue)they're built on volatiles, locks and CAS

the most famous unsafe publication is double-checked locking without volatile, which can hand out a half-constructed singleton. it gets its own chapter: singletons and double-checked locking.

one level deeper:long and double can tear+

JLS §17.7: a write to a non-volatile long or double may be split into two 32-bit writes, so a racing reader could see the high half of one value and the low half of another. on a 64-bit HotSpot this doesn't happen in practice, but the spec allows it. volatile long and volatile double are always atomic. one more reason not to share plain fields.

keylines
  • your intuition is sequential consistency. no CPU and no JIT actually gives you that for free.
  • a write is only guaranteed visible to a read if it happens-before it.
  • edges come from locks (unlock → lock), volatile (write → read), start, join, and transitivity.
  • no data races means the whole program behaves sequentially consistent. that's the deal.
  • volatile gives visibility and ordering, never atomicity.
  • final fields are safe even through a race. everything else needs safe publication.