Skip to main content
Java · Topic 13 of 18

The Java Memory Model (JMM)

Definition​

The Java Memory Model is the set of rules that says when one thread is guaranteed to see what another thread wrote. Actions such as volatile writes, releasing a lock, and starting or joining a thread make earlier writes visible.

Why it exists​

CPUs and compilers cache and reorder operations to run faster, so threads can see old or out-of-order values. The rules give the same guarantees on every machine.

How it works​

Why you can see stale or reordered values​

Modern systems optimise aggressively:

  • CPU caches and store buffers: a core writes to its own cache; other cores may not see it yet.
  • Compiler/JIT reordering and hoisting: the compiler may reorder independent statements, keep a field in a register, or hoist a read out of a loop.
  • CPU out-of-order execution.

Single-threaded semantics are preserved ("as-if-serial"), but another thread can observe the operations in a different order or not at all.

class Broken {
boolean ready; // not volatile
int value;

void writer() { value = 42; ready = true; }

void reader() {
while (!ready) { } // the JIT may hoist the read: this loop can spin forever
System.out.println(value); // even if it exits, may print 0 (reordered writes)
}
}

Making ready volatile fixes both problems.

Happens-before​

The JMM is defined in terms of a partial order called happens-before (HB). If action A happens-before B, then A's effects are visible to B and A is ordered before B. If there is no HB relationship between a write and a read of the same variable, the read may see the old value, the new value, or (for non-volatile long/double on old 32-bit JVMs) a torn value: this is a data race.

The rules that create HB edges:

RuleGuarantee
Program orderWithin one thread, each action HB the next in program order
Monitor lockAn unlock of a monitor HB every later lock of the same monitor
VolatileA write to a volatile variable HB every later read of it
Thread startThread.start() HB every action in the started thread
Thread join / terminationAll actions in a thread HB another thread's successful join() on it
Interruptioninterrupt() HB the interrupted thread detecting it
Final fieldsSpecial rule (below)
TransitivityA HB B and B HB C ⇒ A HB C
Executors/concurrent collectionsSubmitting a task HB its execution; actions before putting into a BlockingQueue/ConcurrentHashMap HB actions after retrieving

Transitivity makes volatile powerful: in the earlier example, value = 42 (program order) HB the volatile write of ready; that write HB the volatile read that sees true; so the reader is guaranteed to see value == 42.

volatile semantics​

  • A volatile read acts as an acquire, a volatile write as a release: operations before the write cannot be reordered after it; operations after the read cannot be moved before it.
  • Reads and writes of volatile long/double are atomic.
  • It does not provide mutual exclusion or atomic compound actions (see Synchronization).

synchronized and visibility​

Mutual exclusion is half of what a monitor gives; the other half is visibility. Releasing the lock flushes writes; acquiring it sees all writes made before the previous release. This is why reads of shared mutable state must also be synchronised on the same lock, otherwise there is no HB edge, even if the writer synchronised.

final field semantics and safe publication​

If an object is constructed correctly (this does not escape) and its fields are final, any thread that sees a reference to it sees those fields fully initialised, even if the reference was published through a data race. Non-final fields get no such guarantee: a racy publication may expose a half-constructed object.

class Holder {
private final int a; // visible correctly even through an unsynchronised publication
private int b; // may be seen as 0 by another thread that gets the reference racily
Holder() { a = 1; b = 2; }
}

Safe publication idioms: store the reference in a final field, a volatile field or AtomicReference, guard it with a lock, put it in a concurrent collection or BlockingQueue, or hand it over via Thread.start/join or an executor. Never leak this from a constructor (starting a thread or registering a listener inside it).

Atomicity of reads and writes​

Reads/writes of references and primitives (except long and double on legacy 32-bit VMs) are atomic, but atomicity is not visibility, and i++ is read-modify-write: three actions, not atomic.

Out-of-thin-air and tearing​

The JMM forbids "out-of-thin-air" values: a racy read returns a value that was actually written by some thread. With long/double non-volatile on 32-bit JVMs, the halves may tear; on modern 64-bit HotSpot this does not happen but the spec still allows it.

System design considerations​

  • Publish shared objects immutably or through a thread-safe holder
  • Use a single documented mechanism per shared field (lock, volatile or atomic)
  • Stress-test on multiple cores, but do not treat a green run as proof

False sharing (a performance, not correctness, issue)​

CPUs move memory in cache lines (typically 64 bytes). If two threads write to different variables that sit on the same cache line, the line ping-pongs between cores, throttling both. Symptom: independent counters that scale badly. Fixes: padding (JDK uses @Contended internally in LongAdder's cells and ForkJoinPool; user code needs -XX:-RestrictContended), splitting data per thread, or using LongAdder.

When to use it, and when not​

Use it when

  • You reason about visibility of shared fields across threads
  • You publish objects safely (final fields, volatile, locks)
  • You review lock-free or double-checked code

Think twice when

  • You rely on timing or sleeps to make code appear correct
  • You share mutable objects without any synchronisation
  • A standard concurrent utility solves the problem

Reasoning checklist​

  1. Which variables are shared and mutable?
  2. For each, what HB edge connects every write to every read? (lock, volatile, join, executor hand-off, concurrent collection)
  3. Is every compound action atomic (single lock, atomic class)?
  4. Is each object safely published (final fields, volatile, lock)?
  5. Can the design avoid sharing altogether?

If you cannot name the HB edge, you have a data race.

Common mistakes​

Double-checked locking, revisited​

Without volatile, another thread can see a non-null instance reference before the constructor's writes become visible (the reference store may be reordered before the field stores). volatile forbids that reordering. The holder idiom avoids the issue because class initialisation is guaranteed thread-safe by the JVM.

  • Reading shared state without the same lock or volatile used for writing.
  • Publishing objects by assigning to a plain static or instance field.
  • Letting this escape during construction.
  • Assuming "it works on my machine" (x86 is a strong memory model; ARM exposes more races).
  • Sleeping or printing to "fix" a visibility problem.

Interview questions​

1. What is the Java Memory Model?

A specification of which values a read may observe and what reorderings are legal, defined through the happens-before relation, so that correctly synchronised programs behave consistently across hardware and JIT optimisations.

2. What does happens-before mean?

If A HB B, A's writes are visible to B and A is ordered before B. Without an HB path between a write and a read, the read may see a stale value.

3. Does volatile make count++ thread-safe?

No: it gives visibility/ordering but not atomicity of the read-modify-write; use atomics or locks.

4. How does final help thread safety?

Final fields of a properly constructed object are visible to any thread that obtains the reference, even without synchronisation, which is why immutable objects are safely shareable.

5. Why is double-checked locking broken without volatile?

The reference can become visible before the object's fields are initialised due to reordering; volatile prevents it.

6. What is a data race?

Two threads accessing the same variable, at least one writing, with no happens-before ordering between the accesses.

7. What is false sharing?

Different threads modifying independent variables on the same cache line, causing cache-coherence traffic that degrades performance; fixed with padding/striping.

8. What is safe publication?

Making an object reference available to other threads in a way that guarantees they see its fully constructed state: via volatile, final, locks, concurrent collections or thread start/join.

Check yourself​

Quick quiz3 questions

1What does a happens-before edge guarantee?

2Why was classic double-checked locking broken without volatile?

3Which gives safe publication of an object's final fields?

Key takeaways​

  • Visibility needs a happens-before edge: locks, volatile, start/join, concurrent collections.
  • volatile + transitivity publishes whole sets of prior writes.
  • Final fields and immutable objects publish safely.
  • If you cannot identify the HB edge, you have a bug waiting to happen.
Next topicExecutors and thread pools

Finished this page? Track your progress (saved only in your browser).