Understand the Java Memory Model, happens-before relationships, and when volatile is the right tool.
Published February 13, 2025
The Java Memory Model (JMM) defines how threads interact through shared memory. Without it, modern CPUs and compilers are free to reorder instructions and cache values in registers — leading to surprising concurrency bugs.
// Thread 1 // Thread 2
boolean flag = false; while (!flag) { } // may loop forever!
// ...
flag = true;
Without volatile, the JVM may cache flag in Thread 2's register. Thread 1's write to flag is invisible to Thread 2.
private volatile boolean running = true;
// Thread 1: always reads the latest value from main memory
public void run() {
while (running) {
doWork();
}
}
// Thread 2: write is immediately visible to all threads
public void stop() {
running = false;
}
volatile guarantees:
volatile does NOT guarantee atomicity: counter++ on a volatile int is still a race condition.
The JMM defines happens-before rules that guarantee memory visibility:
t.start() happens-before any action in thread tt happen-before t.join() returnsprivate volatile int value = 0;
private String data = null;
// Thread 1
data = "hello"; // write to data
value = 1; // volatile write — creates happens-before
// Thread 2
if (value == 1) { // volatile read — sees value = 1
// data is GUARANTEED to be "hello" here
// because volatile write happens-before volatile read
System.out.println(data); // safe!
}
CPUs and compilers reorder instructions for performance. The JMM allows this as long as the observable result within a single thread is the same. volatile inserts memory fences that prevent reordering.
// BROKEN without volatile (reordering can expose partially constructed object)
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // can be reordered!
}
}
}
return instance;
}
}
// CORRECT — volatile prevents the partial construction bug
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
✅ Use volatile when:
❌ Do NOT use volatile when:
AtomicInteger, synchronized)check-then-act (use locks)volatile keyword is about visibility, not synchronization. Knowing this distinction separates candidates.synchronized provides both visibility AND atomicity; volatile only provides visibility.AtomicInteger provides both visibility AND atomic compound operations via CAS (Compare-And-Swap).