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
volatileonpublished, thread B might seepublished = truebut still readx = 0due 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
}
}
| Guarantee | volatile | synchronized | AtomicInteger |
|---|---|---|---|
| Visibility | Yes | Yes | Yes |
| Atomicity of single read/write | Yes | Yes | Yes |
| Atomicity of compound ops (count++) | No | Yes | Yes |
| Mutual exclusion (only one thread at a time) | No | Yes | No |
| Performance overhead | Very low | Medium | Low |
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 asynchronizedblock 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.hostandconfig.portseparately is not safe with just a volatile reference. Use an immutable config object orsynchronizedfor 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:
- It forces all pending writes to be flushed from store buffers to main memory (L3 cache / RAM).
- 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
volatilewrite cannot be moved after it. - Loads after a
volatileread 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
enumsingleton or initialization-on-demand holder idiom for simplicity.
Related Topics
- Java Memory Model — the formal rules that define visibility and ordering for all Java threads
- Synchronization — mutex locks for mutual exclusion and atomicity
- Deadlock — a common pitfall when over-using synchronized blocks
- Callable & Future — higher-level concurrency building blocks
- ReentrantLock & Monitors — explicit locks as an alternative to synchronized
- Multithreading — the foundation for understanding why volatile matters