The Java Memory Model and happens-before

JVM · lesson 27 of 34 · 7 min read

Why a program without synchronisation can be wrong even on one core.

Open this lesson in the learning hub

Key points

  • The JMM is not about caches. It is a set of rules saying which writes a read is guaranteed to see - and without a rule, the compiler and CPU are free to reorder.
  • The compiler, the JIT and the processor all reorder instructions. That is legal as long as a single thread cannot tell; another thread very much can.
  • happens-before is the whole contract. Unlocking a monitor happens-before locking it. A volatile write happens-before a subsequent volatile read of the same field. Starting a thread happens-before anything it does.
  • Without a happens-before edge, a reader may see a stale value indefinitely - not briefly. A loop reading a non-volatile flag can be hoisted out entirely by the JIT and never terminate.
  • volatile gives visibility and ordering but not atomicity: count++ on a volatile field is still read-modify-write and still loses updates.
  • Safe publication is the practical rule: an object handed to another thread must be published through a final field, a volatile, a lock, or a concurrent collection - or the reader may see it half-constructed.

Example

// BROKEN. This can loop forever even though another thread sets the flag.
class Worker implements Runnable {
    private boolean stop = false;              // no happens-before edge

    public void run() {
        while (!stop) { }                      // JIT may hoist the read out
    }                                          //   -> while (true) { }
    public void stop() { stop = true; }
}

// CORRECT. volatile creates the edge: the write is visible to the read.
class Worker2 implements Runnable {
    private volatile boolean stop = false;

    public void run() { while (!stop) { } }
    public void stop() { stop = true; }
}

// UNSAFE PUBLICATION - the reader can see a partly built object.
class Holder { private Config config;
    void init() { config = new Config(loadAll()); }   // not final, not volatile
    Config get() { return config; }   // may return a Config whose fields are null
}

// SAFE - final fields are guaranteed visible once the constructor returns.
class Holder2 {
    private final Config config;
    Holder2() { this.config = new Config(loadAll()); }
    Config get() { return config; }
}

// volatile is NOT atomic. Both of these lose updates:
volatile int count;
void bump() { count++; }              // read, add, write - three steps

// Use an atomic, or a lock:
final AtomicInteger safe = new AtomicInteger();
void bumpSafely() { safe.incrementAndGet(); }

Without a happens-before edge a reader may never see a write - volatile fixes visibility and ordering, never atomicity.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the JVM course, and every lesson in it is listed on the JVM contents page.