volatile is one of Java's most misunderstood keywords. It looks small, even decorative — yet leave it out in the wrong place and your multithreaded code will fail in ways that are nearly impossible to reproduce. This post shows exactly what it does, with diagrams.
The Problem: CPU Caches
Modern CPUs don't read and write straight to RAM — that would be far too slow. Each core has its own L1/L2 cache. When a thread writes a variable, the value lands in the local cache first and may not reach main memory for some time. Meanwhile, another thread on a different core reads from its cache and sees a stale value.
Thread B never sees Thread A's write. It loops forever or creates a duplicate instance.
volatile forces every write through to main memory and every read from it. Both threads agree.
The Hidden Threat: Instruction Reordering
Visibility is only half the story. The JVM and CPU are allowed to reorder instructions for performance — as long as the result looks correct to a single thread. The moment a second thread is involved, this silent optimization becomes a silent disaster.
Why reordering is allowed
The line instance = new Singleton() is not one operation. The JVM breaks it into three:
Step 1. Allocate raw memory on the heap → returns address 0x7f3a
Step 2. Run the constructor → initializes all fields
Step 3. Assign the address to instance → instance = 0x7f3a
From Thread A's perspective, steps 2 and 3 can be swapped freely — the end result is the same for Thread A. So the JVM does exactly that. From another thread's perspective, this is catastrophic.
The race: step by step
Here is what happens when two threads hit getInstance() at the same time,
without volatile:
Thread B sees a non-null pointer and skips the lock entirely — but Thread A hasn't run the constructor yet. Thread B now holds a reference to an object with uninitialized fields.
NullPointerException or wrong field values deep inside
innocent-looking code.
How volatile closes the window
Marking instance as volatile inserts a memory barrier
before the assignment. A memory barrier is a hard CPU instruction that says:
all prior writes must complete and be visible before this line executes.
The JVM is no longer allowed to move the assignment before the constructor.
The barrier forces steps 1→2→3 to stay in order. By the time instance is non-null, the constructor has always already finished.
In Code: The Double-Checked Singleton
This is the canonical use of volatile in Java — the double-checked locking pattern
for a thread-safe Singleton:
public class Singleton {
// volatile: visibility guarantee + prevents reordering
private static volatile Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) { // 1st check — no lock, fast path
synchronized (Singleton.class) {
if (instance == null) { // 2nd check — safe creation
instance = new Singleton();
}
}
}
return instance;
}
}
volatile prevents a thread from reading a half-constructed object due to reordering.
Remove either one and the pattern breaks.
What volatile Is Not
volatile is frequently confused with synchronized. They solve different problems:
| Feature | volatile | synchronized |
|---|---|---|
| Visibility | ✅ Always flushes to main memory | ✅ Yes, on release/acquire |
| Reordering prevention | ✅ Memory barrier | ✅ Full fence |
| Atomicity | ❌ Only for single reads/writes | ✅ Entire block is atomic |
| Mutual exclusion | ❌ No lock, no blocking | ✅ Only one thread at a time |
| Performance | Lightweight | Heavier (lock acquisition) |
volatile int counter = 0;
// Two threads running this concurrently:
counter++; // ❌ Not atomic! This is read → increment → write
// Both threads can read 0, both write 1. You lose an increment.
// Use AtomicInteger instead:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // ✅ atomic CAS operation
Summary
volatile is a targeted, lightweight tool for one specific job:
making a single variable's reads and writes visible and ordered across threads.
It is not a general-purpose lock. Use it when:
- One thread writes, others only read (e.g. a flag, a singleton reference)
- You need to prevent reordering around construction (double-checked locking)
- The operation on the variable is a single read or write, not a compound action
For anything involving compound operations — increment, compare-and-swap, check-then-act —
reach for synchronized, AtomicInteger, or the java.util.concurrent toolkit instead.
volatile guarantees what a thread sees. synchronized guarantees when only one thread acts.