Java Concurrency

The volatile Keyword

What it does, why you need it, and where it falls short

By Md Omar Faroque

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.

Core 1 — Thread A (Writer)
L1 Cache
instance0x7f3a
Core 2 — Thread B (Reader)
L1 Cache
instancenull
⬇ written to local cache only
⬆ reads stale null from own cache
Main
Memory
instancenull

Thread B never sees Thread A's write. It loops forever or creates a duplicate instance.

Core 1 — Thread A (Writer)
L1 Cache
instance0x7f3a ✓
Core 2 — Thread B (Reader)
L1 Cache
instance0x7f3a ✓
⬇ flushed to main memory immediately
⬆ always reads from main memory
Main
Memory
instance0x7f3a

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 A   (writer)
Thread B   (reader)
1 Allocate memory → 0x7f3a
2 Assign instance = 0x7f3a constructor has NOT run yet
3 Check: instance != null? ✓ TRUE — 0x7f3a is non-null
4 Skip sync block, return instance returns 0x7f3a — uninitialized!
5 Run constructor → init fields
6 Call methods on broken object 💥

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.

⚠ Why this is so hard to debug
This race has an extremely narrow window — just the few nanoseconds between Thread A's assignment and its constructor finishing. It won't reproduce in tests. It surfaces under production load, randomly, with 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.

Thread A   (writer)
Thread B   (reader)
1 Allocate memory → 0x7f3a
2 Run constructor → init fields
── volatile memory barrier: constructor must finish before assignment ──
3 Assign instance = 0x7f3a fields fully initialized ✓
4 Check: instance != null? ✓ TRUE — and fully constructed
5 Return instance — safe to use ✓

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;
    }
}
✓ Why both are needed
synchronized prevents two threads from both passing the null check and creating two instances.
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:

Featurevolatilesynchronized
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:

For anything involving compound operations — increment, compare-and-swap, check-then-act — reach for synchronized, AtomicInteger, or the java.util.concurrent toolkit instead.

One-line rule
volatile guarantees what a thread sees. synchronized guarantees when only one thread acts.