Q36 of 40 · Core Java
How does the volatile keyword interact with the JMM? When is it sufficient and when isn't it?
Core JavaSeniorvolatilejmmvisibilityatomicityconcurrencyhappens-before
Short answer
Short answer: A volatile field establishes a happens-before edge between its write and all subsequent reads: the write flushes to main memory and the read always loads from main memory, bypassing CPU caches. This guarantees visibility and prevents instruction reordering around the volatile access. It is sufficient for single-write visibility patterns (flags, one-time publication) but insufficient for compound operations like check-then-act or read-modify-write.
Detail
What volatile guarantees
- Visibility — a write to a volatile field is immediately visible to all subsequent reads of that field.
- Ordering — the JVM/CPU cannot reorder writes before a volatile write to happen after it, nor reorder reads after a volatile read to happen before it (store and load fence semantics).
// Pattern 1: stop flag — volatile IS sufficient
// Thread 1 writes; Thread 2 reads. Single writer, no compound operation.
class TestWorker implements Runnable {
private volatile boolean stopped = false;
public void stop() { stopped = true; } // single write, no race
@Override
public void run() {
while (!stopped) { // always reads latest
doWork();
}
}
}
// Pattern 2: safe publication — volatile IS sufficient
// The volatile write of 'instance' creates an HB edge that makes the
// constructor's writes visible to any thread reading instance != null.
class Config {
private static volatile Config instance;
private final java.util.Map<String, String> props;
private Config() {
props = loadFromFile();
}
public static Config get() {
if (instance == null) {
synchronized (Config.class) {
if (instance == null) instance = new Config(); // volatile write
}
}
return instance;
}
}
import java.util.concurrent.atomic.AtomicInteger;
// Pattern 3: counter — volatile NOT sufficient
private volatile int count = 0;
// count++ decompiles to: read → increment → write
// Two threads can each read 0, each add 1, each write 1 → lost update
void increment() { count++; } // BROKEN
// Fix: use AtomicInteger (CAS instruction, single hardware op)
private final AtomicInteger count = new AtomicInteger(0);
void increment() { count.incrementAndGet(); } // CORRECT
import java.util.concurrent.atomic.AtomicBoolean;
// Pattern 4: check-then-act — volatile NOT sufficient
private volatile boolean hasWork = false;
void process() {
if (hasWork) { // read
hasWork = false; // write — another thread can interleave here
doWork();
}
}
// Fix: AtomicBoolean.compareAndSet(true, false)
AtomicBoolean hasWorkAtomic = new AtomicBoolean(false);
void processSafe() {
if (hasWorkAtomic.compareAndSet(true, false)) {
doWork();
}
}
Summary rule: volatile = visibility + ordering for individual reads/writes. If your correctness depends on combining a read and a write as an atomic unit, you need synchronized, ReentrantLock, or an Atomic class.
// WHAT INTERVIEWERS LOOK FOR
The two guarantees: visibility and reordering prevention. The distinction between visibility and atomicity. Concrete examples of when volatile suffices (stop flag, one-time publication) vs when it doesn't (counter, check-then-act). Strong candidates explain the store-fence and load-fence semantics.
// COMMON PITFALL
Thinking volatile makes operations thread-safe in general. Volatile only prevents stale reads — it has no effect on the atomicity of multi-step operations. The canonical mistake is using a volatile boolean for a compareAndSet pattern.