volatile is the lightest synchronization tool in Java, and the most misunderstood. It guarantees visibility and ordering for a single field — a write by one thread becomes visible to all others, and the compiler/CPU may not reorder around it — but it does not provide atomicity for compound operations. Knowing exactly what it does and does not buy you is fundamental to correct lock-free code.

The visibility problem it solves

Without volatile, a thread may cache a field in a register or per-core cache and never see another thread's update. The canonical broken example is a stop flag:

private boolean running = true;         // BROKEN without volatile
public void run() { while (running) work(); }
public void stop() { running = false; } // other thread may never see this

The worker thread can hoist running into a register and loop forever, never observing stop(). Marking the field volatile forces every read to come from and every write to go to main memory, so the update propagates. Every read sees the most recent write.

Advertisement

Visibility is not atomicity

The trap: volatile does not make read-modify-write operations atomic. count++ is three steps — read, add, write — and two threads can interleave them and lose an increment, volatile or not:

private volatile int count;
count++;   // STILL not atomic -- lost updates under concurrency

For an atomic counter use AtomicInteger (a CAS loop) or a lock. Use volatile only when writes do not depend on the current value — a flag you set, a reference you publish, a timestamp you overwrite. If the new value is a function of the old, volatile is insufficient.

The happens-before guarantee

Since Java 5, volatile carries a memory-ordering guarantee beyond the single field. A write to a volatile field happens-before every subsequent read of it, and — critically — everything the writing thread did before the volatile write is visible to a thread that reads the volatile afterwards. So a volatile field can safely publish a whole object graph built before the write, which is the mechanism behind safe lazy publication.

Advertisement

The double-checked-locking fix

The famous broken singleton needed volatile to become correct. Without it, a reader could see a non-null reference to a partially-constructed object, because the constructor's writes could be reordered after the reference assignment:

private static volatile Singleton instance;  // volatile is mandatory
public static Singleton get() {
    if (instance == null) {                  // first check (no lock)
        synchronized (Singleton.class) {
            if (instance == null)            // second check (locked)
                instance = new Singleton();  // volatile write publishes it safely
        }
    }
    return instance;
}

The volatile prevents the reordering that would let another thread see the reference before the object's fields are initialised. (For a lazy singleton, the initialization-on-demand holder idiom is cleaner still — but where double-checked locking is used, volatile is not optional.)

volatile vs synchronized vs Atomic

Where each fits:

ToolVisibilityAtomicityBlocks?
volatileyesno (single read or write only)no
Atomic*yesyes (one variable, CAS)no
synchronizedyesyes (whole block)yes

Use volatile for a simple flag or safe publication of a reference; Atomic* when you need atomic update of one variable without a lock; synchronized when the atomic unit spans multiple fields or operations. Reaching for volatile where you actually need atomicity is the most common concurrency bug in the wild.

volatile guarantees visibility and ordering for one field, plus a happens-before edge that lets it safely publish objects — but it does NOT make compound operations like ++ atomic. Use it for flags, safe publication, and the double-checked-locking singleton. When the new value depends on the old, use AtomicInteger; when the atomic unit spans multiple fields, use synchronized.