The Java Memory Model and happens-before
Why a program without synchronisation can be wrong even on one core.
Open this lesson in the learning hubKey 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.
volatilegives 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.