Skip to content
Java synchronization 6 min read

volatile Keyword

The volatile keyword is Java’s lightweight way to tell the JVM: “this variable is shared across threads — always read it from main memory, never from a local CPU cache.” It is simpler than full synchronization, but it solves a narrower problem: visibility, not atomicity.

The Problem volatile Solves

Modern CPUs and the JVM are free to cache variables in CPU registers or local caches for performance. When one thread updates a variable, another thread may keep reading a stale cached copy — and never see the change.

public class FlagExample {
    // Without volatile, the loop may NEVER terminate
    private boolean running = true;

    public void stop() {
        running = false; // written by thread A
    }

    public void run() {
        while (running) { // read by thread B — may read stale cached value
            // do work
        }
        System.out.println("Stopped.");
    }
}

Without volatile, thread B can cache running = true in a register and spin forever, even after thread A sets it to false. Adding volatile fixes this.

private volatile boolean running = true;

Now every read of running goes directly to main memory, and every write is immediately flushed back.

Syntax

private volatile int counter;
private volatile boolean flag;
private volatile MyObject sharedRef;

Just add the volatile modifier before the type — that is all the syntax required.

What volatile Guarantees

Visibility

A write to a volatile variable by thread A is guaranteed to be visible to thread B the next time thread B reads that variable. No stale caching.

public class SharedState {
    private volatile int value = 0;

    // Thread A
    public void writer() {
        value = 42; // flushed to main memory immediately
    }

    // Thread B
    public void reader() {
        System.out.println(value); // always reads from main memory
    }
}

Happens-Before Ordering

volatile establishes a happens-before relationship. Everything thread A did before writing to the volatile variable is visible to thread B after it reads that variable. This prevents instruction reordering around volatile accesses.

public class PublicationExample {
    private int x = 0;
    private volatile boolean published = false;

    // Thread A
    public void publish() {
        x = 100;           // happens-before...
        published = true;  // ...volatile write
    }

    // Thread B
    public void consume() {
        if (published) {           // volatile read
            System.out.println(x); // guaranteed to print 100
        }
    }
}

Note: Without volatile on published, thread B might see published = true but still read x = 0 due to instruction reordering.

What volatile Does NOT Guarantee

volatile does not make compound operations atomic. The classic counter example shows why:

public class BrokenCounter {
    private volatile int count = 0;

    // NOT thread-safe! read-modify-write is not atomic
    public void increment() {
        count++; // expands to: read count, add 1, write count
    }
}

Two threads can both read count = 5, both add 1, and both write 6 — losing one increment. For this you need synchronized, or better, AtomicInteger from java.util.concurrent.atomic.

import java.util.concurrent.atomic.AtomicInteger;

public class SafeCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // atomic, no lock needed
    }
}
GuaranteevolatilesynchronizedAtomicInteger
VisibilityYesYesYes
Atomicity of single read/writeYesYesYes
Atomicity of compound ops (count++)NoYesYes
Mutual exclusion (only one thread at a time)NoYesNo
Performance overheadVery lowMediumLow

When to Use volatile

volatile is the right tool when:

  • One thread writes, many threads read. A flag, a status indicator, a “stop” signal.
  • Writes are independent. The new value does not depend on the current value (no count++).
  • You need publication safety. Safely publishing a reference after fully constructing an object.

A typical real-world pattern is a stop/shutdown flag:

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

    public void shutdown() {
        shutdown = true;
    }

    @Override
    public void run() {
        while (!shutdown) {
            // process tasks
        }
        System.out.println("Worker stopped cleanly.");
    }
}

Tip: If you find yourself needing atomic compound operations, reach for AtomicInteger, AtomicReference, or a synchronized block instead.

volatile with References

volatile applies to the reference itself, not to the object’s fields. Making a reference volatile guarantees visibility of which object the reference points to — but not the fields inside that object.

public class Config {
    public String host;
    public int port;
}

public class Server {
    private volatile Config config; // volatile on the reference

    // Thread A — safe: replaces the whole object
    public void updateConfig(Config newConfig) {
        config = newConfig;
    }

    // Thread B — safe: reads a consistent reference
    public void connect() {
        Config c = config; // single volatile read — cache locally
        System.out.println(c.host + ":" + c.port);
    }
}

This pattern (replacing the whole object rather than mutating fields) is common in lock-free configuration updates.

Warning: Mutating config.host and config.port separately is not safe with just a volatile reference. Use an immutable config object or synchronized for that.

Under the Hood

CPU Memory Barriers

When the JVM compiles code with a volatile write, it inserts a memory barrier (also called a memory fence) instruction. On x86 this is often an MFENCE or LOCK prefix. On ARM it is a DMB (Data Memory Barrier).

A memory barrier does two things:

  1. It forces all pending writes to be flushed from store buffers to main memory (L3 cache / RAM).
  2. It invalidates the local CPU cache line for that address so future reads go back to main memory.

This is why volatile reads/writes are slightly more expensive than regular variable access, but far cheaper than acquiring a lock.

JIT and Instruction Reordering

The Java JIT compiler aggressively reorders instructions for performance. volatile acts as a reordering fence:

  • Stores before a volatile write cannot be moved after it.
  • Loads after a volatile read cannot be moved before it.

This is precisely what enables the happens-before guarantee from the Java Memory Model.

Bytecode

At the bytecode level, volatile is encoded as an ACC_VOLATILE flag on the field in the class file. The JVM spec mandates that compliant JVMs honor this flag. You can inspect it with javap -verbose ClassName.

Double-Checked Locking Pattern (Java 5+)

A famous use of volatile is in the double-checked locking singleton pattern, which was broken before Java 5’s strengthened memory model:

public class Singleton {
    // volatile is essential here — without it, partially constructed
    // objects can be seen by other threads
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                  // first check (no lock)
            synchronized (Singleton.class) {
                if (instance == null) {          // second check (with lock)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Without volatile, a thread could observe instance != null but see an incompletely initialized object due to instruction reordering during construction. The volatile write on instance = new Singleton() acts as a full memory barrier, ensuring the object is fully constructed before the reference is published.

Note: This pattern is safe and correct from Java 5 onward. If you are on modern Java, prefer an enum singleton or initialization-on-demand holder idiom for simplicity.

Last updated June 13, 2026
Was this helpful?