The 30 second answer
The memory model defines when one thread's writes become visible to another. Without a happens-before edge, the compiler and CPU may reorder or cache values, so a thread can see stale or half-published data. Edges come from program order, unlocking then locking the same monitor, a volatile write then a read of it, starting and joining threads, and final fields set in a constructor.
The problem it solves. Each thread may run on a core with its own cache, and the JIT and CPU reorder instructions when a single thread cannot tell the difference. Another thread can. So "I wrote the field before setting the flag" does not mean another thread sees them in that order. The Java Language Specification, chapter 17, defines which orderings a correct program can rely on.
Happens-before. The JLS says that if one action happens-before another, the first is visible to and ordered before the second. The edges you use every day:
- Program order: within one thread, each action happens-before the next.
- Monitor lock: an unlock of a monitor happens-before every later lock of the same monitor.
- Volatile: a write to a volatile field happens-before every later read of that field.
- Thread start and join:
start()happens-before the started thread's actions; all of a thread's actions happen-before another thread returns fromjoin()on it. - Transitivity: chain them. If A happens-before B and B happens-before C, A happens-before C.
Separately, final fields set in a constructor are seen correctly by other threads once the constructor finishes, provided this did not escape during construction.
Three properties to keep apart. Visibility (does the other thread see the write), ordering (in what order), and atomicity (is a compound action indivisible). volatile gives the first two, not the third: count++ on a volatile field still loses updates. That difference is covered in volatile and AtomicInteger.
Safe publication. The plain field below is published through the volatile flag: the write to data happens-before the volatile write, which happens-before the reader's volatile read.
public class Visibility {
private static volatile boolean running = true; // remove volatile and the loop may never stop
private static int data; // plain field, published by the volatile write
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) { Thread.onSpinWait(); }
System.out.println("saw data=" + data); // guaranteed 42: write data hb volatile write hb volatile read
});
worker.start();
Thread.sleep(50);
data = 42;
running = false;
worker.join();
}
}Verified with OpenJDK 21.0.13: it prints saw data=42. Without volatile, the loop is allowed never to see the flag change.
Data race. Two accesses to the same variable, at least one a write, not ordered by happens-before. A program with no data races behaves as if sequentially consistent; that is the guarantee to aim for.
Alternate interview scenarios
"A double-checked locking singleton works on your laptop but sometimes hands out a half-built object. Why?" Without volatile on the instance field, the write of the reference can become visible before the constructor's writes, so another thread sees a non-null reference to an object whose fields are not yet set. Declaring the field volatile adds the happens-before edge. Simpler still: a holder class or an enum, whose initialisation the JVM already makes thread-safe.