volatile - What It Guarantees (and What It Doesn't)

The volatile keyword in Java provides visibility and ordering guarantees for a single variable across threads. It matters when one thread writes a flag or state that others must see immediately without holding a lock. This article explains what volatile guarantees, what it does not, and when to use it or prefer synchronized or atomics.

Overview

In a multi-threaded environment, each thread may cache variables in CPU registers or local caches. Without synchronization, a write in one thread might not be visible to another thread for some time—or at all. The volatile keyword addresses this for a single variable:

  • Visibility: A write to a volatile variable is immediately visible to all threads that subsequently read it. The JVM does not cache the value in registers or thread-local storage.
  • Ordering: The Java Memory Model ensures that no instruction is reordered across a volatile read or write. You can use a volatile as a "guard" so that updates made before writing the volatile are visible to a thread that reads the volatile and then continues.
  • What it does NOT do: volatile does not provide atomicity for compound operations (e.g. read-modify-write). Use AtomicInteger, synchronized, or a lock for that.

Example

Example 1: Stopping a background thread with a volatile flag

Java
public class Worker implements Runnable {
    private volatile boolean stopped = false;

    public void stop() {
        stopped = true;  // visible to worker thread
    }

    @Override
    public void run() {
        while (!stopped) {  // always sees latest value
            doWork();
        }
    }
}

Without volatile, the worker thread might cache stopped in a register and never see true when another thread calls stop(). With volatile, the write in stop() is guaranteed to be visible to the loop in run().

Example 2: Safe publication of an immutable object

Java
private volatile Config config;

public void init() {
    Config c = new Config();
    c.load();
    this.config = c;  // volatile write: all prior writes to c are visible after this
}

public Config getConfig() {
    return config;    // volatile read: sees the fully constructed Config
}

The volatile write "releases" the construction of Config; the volatile read "acquires" it. This pattern works when the object is effectively immutable after publication. Do not use volatile for double-checked locking of mutable state; use proper locking or the holder pattern.

Example 3: What volatile does NOT fix — compound action

Java
private volatile int count = 0;

public void increment() {
    count++;  // NOT atomic: read, add 1, write — another thread can interleave
}

count++ is a read-modify-write. Two threads can both read 0, both add 1, both write 1—the result is 1 instead of 2. Use AtomicInteger or synchronized for correct increments.

Example 4: Visibility without atomicity

Java
volatile boolean ready = false;
volatile int value = 0;

// Thread A
value = 42;
ready = true;

// Thread B
while (!ready) { /* spin */ }
System.out.println(value);  // guaranteed to see 42

The volatile on ready ensures that when Thread B sees ready == true, it also sees the write to value that happened before it (happens-before). Without volatile on ready, the compiler or CPU could reorder so that Thread B sees ready == true but value == 0.

Core Mechanism / Behavior

  • Happens-before: A write to volatile v happens-before every subsequent read of v. So any memory write that happens-before the volatile write is visible to a thread that reads v and then continues.
  • No reordering: The compiler and CPU cannot move a normal read/write across a volatile access. So "write x, write volatile flag" is seen in that order by a thread that "read volatile flag, read x".
  • Memory barriers: Volatile reads and writes typically imply memory barriers (e.g. acquire/release semantics), which have a small cost. Use volatile only when you need the guarantee.
ScenarioUse volatile?Alternative
Single flag (e.g. stop)Yes
Publish immutable object onceYesFinal fields, Holder pattern
Counter (read-modify-write)NoAtomicInteger, synchronized
Multiple related fieldsNosynchronized, lock

Key Rules

  • Use volatile for a single variable that one thread writes and others read (e.g. status flag, one-time reference publication).
  • Do not use volatile for compound actions; use atomics or locks.
  • Prefer final fields and safe publication patterns for immutable objects; use volatile when you need to flip a reference or flag after construction.
  • When in doubt between volatile and synchronized, use synchronized (or ReentrantLock) for correctness; optimize later if needed.

What's Next

For the full memory model behind visibility and ordering, see Java Memory Model (JMM). For building custom atomic behavior, see AQS Explained and synchronized vs ReentrantLock.