00:00

volatile Keyword in Java – Visibility and Ordering Between Threads

The volatile keyword tells the JVM that a field is shared between threads, so every read must see the most recent write by any thread. It guarantees visibility and ordering, but not atomicity: count++ on a volatile field is still unsafe.

This tutorial explains the visibility problem volatile solves, the happens-before guarantee it provides, where it is the right tool (flags, safe publication, double-checked locking), and when you need synchronized or atomic classes instead.

1. The Visibility Problem

For speed, CPUs keep values in registers and caches, and the compiler and JIT may reorder or hoist instructions. Without synchronization, the Java Memory Model (JMM) does not guarantee that a value written by one thread is ever seen by another. A classic symptom is a loop that never stops:

Broken: may loop forever
static boolean running = true;     // NOT volatile

// worker thread
while (running) { }                // the JIT may read 'running' once and loop forever

// main thread
running = false;                   // the worker may never see this

2. volatile Fixes Visibility

Declaring the flag volatile forces every read to see the latest write, so the worker stops reliably:

VolatileFlag.java
public class VolatileFlag {
    // Without volatile, the worker may never see the update and loop forever
    private static volatile boolean running = true;

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("Worker: started");
            long iterations = 0;
            while (running) {
                iterations++;          // busy work
            }
            System.out.println("Worker: saw running=false, stopping");
        });
        worker.start();

        Thread.sleep(200);
        System.out.println("Main: requesting stop");
        running = false;               // volatile write: visible to the worker
        worker.join();
        System.out.println("Main: worker finished");
    }
}
Output
Worker: started
Main: requesting stop
Worker: saw running=false, stopping
Main: worker finished

A volatile stop flag (or status flag) written by one thread and read by others is the most common and safest use of volatile.

3. Ordering and Happens-Before

volatile also limits reordering. The JMM rule is: a write to a volatile variable happens-before every later read of that variable. Everything the writing thread did before the volatile write becomes visible to a thread that reads the new value. This lets a volatile flag "publish" ordinary data safely:

SafePublication.java
public class SafePublication {
    private static int[] data;                 // plain field
    private static volatile boolean ready;     // volatile "guard"

    public static void main(String[] args) throws InterruptedException {
        Thread reader = new Thread(() -> {
            while (!ready) Thread.onSpinWait(); // wait for the volatile flag
            // Happens-before: everything written before 'ready = true' is visible here
            System.out.println("Reader sees sum = " + (data[0] + data[1] + data[2]));
        });
        reader.start();

        data = new int[] {10, 20, 30};          // 1. plain writes
        ready = true;                           // 2. volatile write publishes them
        reader.join();
    }
}
Output
Reader sees sum = 60

The array itself is not volatile, yet the reader is guaranteed to see {10, 20, 30}, because the plain writes come before the volatile write in program order.

4. volatile Is Not Atomic

count++ is three steps: read, add one, write. Two threads can read the same value and both write the same result, losing an update. volatile makes each read and write visible but does not make the three steps indivisible:

VolatileNotAtomic.java
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileNotAtomic {
    static volatile int volatileCount = 0;
    static final AtomicInteger atomicCount = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 100_000; i++) {
                volatileCount++;               // read-modify-write: NOT atomic
                atomicCount.incrementAndGet(); // atomic
            }
        };
        Thread t1 = new Thread(task), t2 = new Thread(task);
        t1.start(); t2.start();
        t1.join();  t2.join();

        System.out.println("Expected:       200000");
        System.out.println("volatile int:   " + volatileCount);
        System.out.println("AtomicInteger:  " + atomicCount.get());
    }
}
Output (sample run; the volatile count varies between runs)
Expected:       200000
volatile int:   195607
AtomicInteger:  200000

Use AtomicInteger/AtomicLong/LongAdder, or synchronized, for compound actions such as increments and check-then-act. One exception worth knowing: reads and writes of long and double are not guaranteed to be atomic unless they are volatile, so volatile does make a single 64-bit read or write atomic.

5. Double-Checked Locking

Lazily initialised singletons that avoid locking on every call need volatile. Without it, another thread could see a non-null reference to an object whose constructor has not finished:

LazySingleton.java
public class LazySingleton {
    static class Config {
        private static volatile Config instance;   // volatile is essential here
        final String env;

        private Config() {
            env = "production";
            System.out.println("Config created (constructor ran once)");
        }

        static Config getInstance() {
            Config local = instance;               // one volatile read on the fast path
            if (local == null) {
                synchronized (Config.class) {
                    local = instance;
                    if (local == null) {
                        instance = local = new Config();
                    }
                }
            }
            return local;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[4];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(Config::getInstance);
        }
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        System.out.println("Same instance everywhere: " + (Config.getInstance() == Config.getInstance()));
        System.out.println("env = " + Config.getInstance().env);
    }
}
Output
Config created (constructor ran once)
Same instance everywhere: true
env = production

The local variable saves a second volatile read on the fast path. For most singletons, a simpler alternative is the holder idiom (a static nested class holding the instance) or an enum.

6. volatile vs synchronized vs Atomic Classes

FeaturevolatilesynchronizedAtomic classes
VisibilityYesYesYes
Atomic compound actionsNoYes (whole block)Yes (single variable)
Locking / blockingNoYesNo (lock-free CAS)
Applies toFieldsMethods and blocksObjects such as AtomicInteger
Typical useFlags, published referencesMulti-step updates of shared stateCounters, sequence numbers

7. When to Use volatile

  • Writes to the field do not depend on its current value (no x++, no check-then-act), or only one thread ever writes it.
  • The field does not take part in an invariant with other fields.
  • You need no locking while reading.

Good fits: stop/status flags, configuration references replaced as a whole, and safe publication of immutable objects. For anything more complex, prefer the higher-level java.util.concurrent tools.

8. Rules

  • volatile applies only to fields (instance or static), not to methods, classes or local variables.
  • A field cannot be both volatile and final; final fields already have their own visibility guarantee.
  • A volatile reference makes the reference visible, not the fields of the object it points to after later changes.
  • Volatile reads and writes are slightly more expensive than plain ones, but much cheaper than locking.

9. Summary

  • volatile guarantees that writes to a field are visible to all threads and limits reordering.
  • A volatile write happens-before every subsequent read of the same field.
  • volatile does not make compound actions like count++ atomic.
  • Use it for flags, safe publication and double-checked locking; use locks or atomics for compound updates.

Related Tutorials