Java Memory Model: happens-before, Visibility & Instruction Reordering

The Java Memory Model (JMM) defines the rules that govern how threads read and write shared variables. Without understanding the JMM, apparently correct multi-threaded code can fail in unpredictable ways due to CPU caching, compiler optimisations, and instruction reordering. The JMM's core concept is happens-before: a partial ordering that guarantees what one thread has written is visible to another.

Share

Why the JMM Exists: The Visibility Problem

Modern CPUs and compilers may reorder instructions, cache values in registers, or keep thread-local copies of variables in CPU caches. Without explicit synchronisation, Thread A's writes may never become visible to Thread B.

Java
// WARNING: broken — no synchronisation between threads
public class VisibilityBug {
    static boolean ready = false;
    static int     value = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread writer = new Thread(() -> {
            value = 42;
            ready = true;   // compiler may reorder: ready = true before value = 42
        });

        Thread reader = new Thread(() -> {
            while (!ready) { /* spin */ }
            // reader may see ready == true but value == 0 due to reordering!
            System.out.println(value);
        });

        reader.start();
        writer.start();
    }
}
The code above has two bugs: (1) ready written by the writer may never be seen by the spinning reader; (2) the compiler or CPU may reorder value = 42 and ready = true, so the reader could see ready == true but value == 0. Both bugs are fixed by declaring volatile or using synchronisation.

happens-before: The Core JMM Guarantee

If action A happens-before action B, then all effects of A are visible to B. The JMM defines these relationships:

Rulehappens-before guarantee
Program orderWithin a single thread, each statement happens-before the next
Monitor lockUnlock of a monitor happens-before every subsequent lock of that monitor
Volatile writeA write to a volatile field happens-before every subsequent read of that field
Thread startThread.start() happens-before any action in the started thread
Thread joinAny action in a thread happens-before Thread.join() returns
Object initialisationStatic initialiser completion happens-before any thread accesses the class
TransitivityIf A hb B and B hb C, then A hb C

volatile: Visibility Without Mutual Exclusion

Declaring a field volatile guarantees: (1) writes are immediately flushed to main memory; (2) reads always fetch from main memory; (3) no reordering of volatile accesses with surrounding reads/writes.

Java
public class VolatileExample {

    // volatile ensures all threads see the latest value
    private volatile boolean ready = false;
    private volatile int     value = 0;

    public void writer() {
        value = 42;     // 1. write value (not volatile — but...)
        ready = true;   // 2. volatile write; JMM guarantees: value=42 visible after ready=true
    }

    public void reader() {
        while (!ready) { /* spin */ }   // 3. volatile read
        // 4. Because ready write hb ready read, value=42 is guaranteed visible here
        System.out.println(value);  // always prints 42
    }
}
A volatile write to field X guarantees that all writes performed by that thread before the volatile write are visible to any thread that subsequently reads X. This is the "piggybacking" rule: you can use a single volatile to publish non-volatile state.

volatile Limitations: Not Atomic for Compound Actions

volatile guarantees visibility but not atomicity. Compound operations like i++ (read-modify-write) are not atomic even on volatile fields.

Java
public class VolatileLimitation {

    volatile int counter = 0;

    // BROKEN: multiple threads calling this will lose updates
    // i++ compiles to: read counter, increment, write counter (3 steps — not atomic)
    public void increment() {
        counter++;
    }

    // CORRECT: use AtomicInteger for atomic compound operations
    java.util.concurrent.atomic.AtomicInteger atomicCounter = new java.util.concurrent.atomic.AtomicInteger(0);

    public void incrementCorrectly() {
        atomicCounter.incrementAndGet();   // single atomic CAS operation
    }
}
ToolVisibilityAtomicityMutual exclusion
volatile✅Simple read/write only❌
synchronized✅✅ (entire block)✅
AtomicInteger etc.✅✅ (single variable)❌
ReentrantLock✅✅ (entire block)✅

synchronized and the JMM

Acquiring a monitor lock establishes a happens-before with the preceding unlock of that same monitor. Everything the previous lock holder wrote is visible to the next acquirer.

Java
public class SynchronizedVisibility {

    private int value = 0;
    private final Object lock = new Object();

    // All writes inside synchronized are flushed when lock is released
    public synchronized void write(int v) {
        value = v;
    }   // unlock happens here — flushes value to main memory

    // All reads inside synchronized see the latest written value
    public synchronized int read() {
        return value;
    }   // unlock on exit

    // Equivalent explicit lock
    public void writeWithLock(int v) {
        synchronized (lock) {
            value = v;
        }   // unlock — happens-before any subsequent lock on 'lock'
    }
}

Safe Publication: Sharing Objects Between Threads

An object is safely published when all its fields are visible to any thread that obtains a reference to it. Unsafe publication allows a thread to see a partially constructed object.

Java
// UNSAFE: another thread might see a partially constructed Config
public class UnsafePublication {
    public static Config instance;   // plain field, not volatile

    public static void init() {
        instance = new Config();     // reference may be visible before constructor completes
    }
}

// SAFE option 1: volatile field
public class VolatilePublication {
    public static volatile Config instance;   // volatile write hb volatile read

    public static void init() {
        instance = new Config();
    }
}

// SAFE option 2: final fields — immutable objects are always safely published
public class SafeImmutable {
    public final int x;
    public final int y;

    public SafeImmutable(int x, int y) {
        this.x = x;
        this.y = y;
    }
    // final fields guaranteed visible after constructor returns — even without volatile
}

// SAFE option 3: static initialiser (class loading is thread-safe)
public class SafeSingleton {
    private static final Config INSTANCE = new Config();  // guaranteed safe

    public static Config get() { return INSTANCE; }
}

// SAFE option 4: double-checked locking with volatile (Java 5+)
public class DCLSingleton {
    private static volatile DCLSingleton instance;

    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}
The final field rule in the JMM (Java 5+) guarantees that after a constructor completes normally, any thread that obtains a reference to the object sees the correct values of final fields without synchronisation, even if the reference was published unsafely.

Instruction Reordering: What the JVM Can Move

The JVM and CPU may reorder instructions as long as the program appears correct within a single thread. Across threads, only happens-before rules prevent reordering.

Reordering typeExamplePrevented by
Compiler reorderHoisting a read out of a loopvolatile, synchronized
CPU store bufferWrite visible to local CPU before other CPUsvolatile (StoreLoad memory barrier)
CPU instruction reorderIndependent instructions swapped for pipeline efficiencyvolatile, synchronized
Read cachingCPU serves read from L1 cache instead of main memoryvolatile forces cache invalidation
Java
// volatile fields create memory barriers
// Writing a volatile field: StoreStore + StoreLoad barriers inserted
// Reading a volatile field: LoadLoad + LoadStore barriers inserted

volatile int flag = 0;

// Thread A
a = 1;          // ordinary write
b = 2;          // ordinary write  (both guaranteed to happen before flag write)
flag = 1;       // volatile write  ← StoreStore barrier before; StoreLoad after

// Thread B
int f = flag;   // volatile read   ← LoadLoad barrier before; LoadStore after
// Any read of a, b here is guaranteed to see a=1, b=2
int x = a;
int y = b;

Common JMM Pitfalls

PitfallSymptomFix
Non-volatile stop flagThread never sees running = falseDeclare volatile boolean running
Unsafe lazy init (no volatile)Reader sees partially constructed objectUse volatile, final, or static holder
volatile for compound actionsLost updates on i++Use AtomicInteger or synchronized
Synchronising on different objectsNo mutual exclusion between threadsAlways lock the same object
Publishing via non-final, non-volatile fieldOther threads see null or stale referencePublish via volatile or final
Assuming sequential consistencyCounter-intuitive orderings on multi-coreUnderstand happens-before; don't assume execution order