Why volatile exists, the happens-before guarantee, and when to use atomic classes.
Published September 21, 2026
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)CPUs don't move memory to cache one variable at a time — they move a whole cache line (typically 64 bytes). If two threads each frequently write to different variables that happen to be laid out within the same cache line (e.g. two int fields next to each other in the same object, written by different threads), every write by one thread invalidates the other thread's cached copy of that entire line — even though the two threads aren't logically sharing any data. Each thread ends up re-fetching from main memory far more often than the actual data dependencies would require, silently destroying throughput without any visible correctness bug. The fix is padding — spacing hot, independently-written fields far enough apart (or onto separate cache lines) that they can't collide; java.util.concurrent.atomic classes and some JDK internals use exactly this technique (@Contended in newer JDKs) to avoid it.
AtomicInteger, AtomicLong, AtomicReference, and friends provide both visibility (like volatile) and atomicity for compound operations (which plain volatile does not) — without taking a lock at all.
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic, unlike counter++ on a volatile int
Internally, this is implemented with CAS (Compare-And-Swap): an atomic CPU instruction that says "set this memory location to a new value, but only if it still holds the value I expect — otherwise, tell me it failed." incrementAndGet() reads the current value, computes the new value, and attempts a CAS; if another thread changed the value in between, the CAS fails and the operation retries the read-compute-CAS cycle until it succeeds. This is why atomic classes are called lock-free rather than lock-based: no thread ever blocks waiting for a lock, they just occasionally retry — under low-to-moderate contention this is significantly cheaper than acquiring a synchronized lock or ReentrantLock, though under very high contention the retry loop itself can start to cost more than a lock would.
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).