volatile
The volatile keyword guarantees that reads and writes to a field are always visible across threads and establishes ordering — but it does not make compound operations atomic.
volatile is a field modifier that gives specific, limited guarantees about how a variable behaves under concurrent access: every read of a volatile field sees the most recent write from any thread, and the compiler and CPU are constrained from reordering certain operations around volatile reads/writes. It is a much lighter-weight tool than synchronized, but it solves a narrower problem — visibility and ordering, not mutual exclusion — and misunderstanding that boundary is one of the most common concurrency bugs in Java.
The Visibility Problem Without volatile
Without any synchronization, the Java Memory Model does not guarantee that a write to a shared field by one thread becomes visible to another thread in any particular time frame — or even ever, from that thread's perspective. This is not really about a value being 'stuck in a CPU cache' in a literal sense so much as it is about what the memory model permits the compiler and runtime to do: absent a happens-before relationship, a thread reading a field repeatedly may keep observing a stale value it read once, because nothing forbids the JIT from hoisting that read out of a loop.
Without volatile (or other synchronization), a JIT-compiled loop() could legally cache 'running' in a register once and never re-check main memory, causing the loop to spin forever even after stop() runs on another thread.
class Flag {
private boolean running = true;
void stop() { running = false; }
void loop() {
while (running) { /* work */ } // may never see running == false
}
}Declaring a volatile Field
volatile can only modify fields (instance or static), never local variables, method parameters, or classes/methods. Adding it is syntactically trivial — the guarantees it provides are what matters.
With volatile, every read of 'running' inside loop() is guaranteed to see the most recent write from any thread, so the loop reliably exits after stop() runs.
class Flag {
private volatile boolean running = true;
void stop() { running = false; }
void loop() {
while (running) { /* work */ } // guaranteed to observe stop()'s write, eventually
}
}Visibility Guarantee
A write to a volatile field happens-before every subsequent read of that same field by any thread. In practice, this means once a thread writes a new value to a volatile field, any other thread that reads that field afterward is guaranteed to observe the new value, not a stale one.
Ordering Guarantees
volatile also constrains instruction reordering: the compiler and processor may not reorder other reads/writes around a volatile write or read in ways that would violate the happens-before relationship. Concretely, all writes that happen-before a volatile write (in program order, on the writing thread) become visible to any thread that performs a matching volatile read afterward — not just the volatile field itself.
Because (2) is a volatile write and (3) is a matching volatile read that observes it, the happens-before relationship also guarantees (1)'s plain write to timeout is visible at (3) — volatile's ordering guarantee extends to surrounding non-volatile writes made before it.
class Config {
private int timeout; // plain field
private volatile boolean ready; // volatile field
void configure() {
timeout = 30; // (1) plain write
ready = true; // (2) volatile write
}
void use() {
if (ready) { // (3) volatile read
System.out.println(timeout); // guaranteed to see 30, not 0
}
}
}What volatile Does Not Provide: Atomicity
volatile guarantees visibility and ordering for individual reads and writes of the field itself — it does not make compound, read-modify-write operations atomic. count++ on a volatile int is still three separate steps (read, add, write), and two threads can still interleave those steps and lose an update, exactly as with a non-volatile field.
volatile ensures each read of count and each write to count is immediately visible, but the read-then-write sequence inside count++ is not protected as a single atomic unit.
This is one of the most common misconceptions about volatile: adding it to a counter field does NOT make count++ thread-safe. Use synchronized, a Lock, or an atomic class (AtomicInteger, covered in its own topic) for compound operations that need to be atomic.
class Counter {
private volatile int count = 0;
void increment() { count++; } // STILL not atomic, despite volatile
}
// Two threads calling increment() 100_000 times each can still
// produce a final count less than 200_000.What volatile Does Not Provide: Mutual Exclusion
volatile does not prevent two threads from executing overlapping code concurrently the way synchronized does. It offers no locking, no critical section, and no way to make a multi-field invariant (e.g. 'x and y must always be updated together') hold consistently — for that, synchronization primitives that actually provide mutual exclusion are required.
volatile vs. synchronized
volatile is lighter weight because it never blocks a thread and has lower overhead than lock acquisition, but it trades away atomicity and mutual exclusion to get that.
volatile synchronized
Visibility yes yes
Ordering yes (for that field) yes (happens-before on lock)
Atomicity of no yes (whole critical section)
compound ops
Mutual exclusion no yes
Blocking never blocks can block (BLOCKED state)
Typical cost cheap read/write lock acquire/release overheadCorrect Use Cases for volatile
volatile is the right tool specifically when a field's individual reads/writes need to be visible across threads and no compound operation on it needs to be atomic. Classic patterns: a boolean shutdown/running flag checked in a loop and set by another thread; a status/reference field that is written once by one thread and read by many (a simple form of publication); a double-checked-locking singleton's instance field (which must be volatile to be correct, as of Java 5+ memory model semantics).
Without volatile here, a reading thread could observe a non-null 'instance' reference whose constructor hasn't fully finished running from that thread's point of view, due to reordering — a subtle bug in the classic double-checked-locking pattern that volatile specifically fixes.
class Singleton {
private static volatile Singleton instance;
static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile prevents unsafe publication
}
}
}
return instance;
}
}volatile and 64-bit Fields (long, double)
The Java Language Specification does not guarantee that reads/writes of non-volatile long and double fields are atomic — on some JVM implementations a 64-bit value could theoretically be read as a mix of two separate 32-bit writes ('word tearing'). Declaring a long or double field volatile guarantees its individual reads and writes are atomic (no tearing), in addition to the usual visibility/ordering guarantees.
Practical Usage: Shutdown Flags
A very common, correct use of volatile is a cooperative shutdown flag for a worker thread's loop.
requestShutdown() is a single, simple write (no compound operation), and the loop's read is a single read — exactly the pattern volatile is designed for.
class Worker implements Runnable {
private volatile boolean shutdownRequested = false;
public void run() {
while (!shutdownRequested) {
// do work
}
System.out.println("worker exiting cleanly");
}
void requestShutdown() { shutdownRequested = true; }
}Key Takeaways
volatile guarantees visibility: a write to a volatile field is visible to any thread that subsequently reads it.
volatile also provides ordering guarantees, preventing certain reorderings around the volatile read/write and extending happens-before to surrounding plain writes.
volatile does NOT make compound operations like count++ atomic — read-modify-write sequences on a volatile field can still race.
volatile does NOT provide mutual exclusion — it never blocks a thread and cannot protect multi-step invariants.
volatile guarantees atomic (non-torn) reads/writes of long and double fields, which non-volatile 64-bit fields are not guaranteed to have.
Good use cases: simple status/shutdown flags, one-time or infrequent publication of a reference, the instance field in double-checked-locking singletons.
private volatile int count = 0; void increment() { count++; } // still racy!
private final AtomicInteger count = new AtomicInteger(0); void increment() { count.incrementAndGet(); } // atomic read-modify-write volatile does not make count++ atomic; it's still a separate read, add, and write. Use AtomicInteger (or synchronized) when a compound update must be atomic.
private volatile List<String> items = new ArrayList<>(); void addItem(String s) { items.add(s); } // volatile only protects the reference, not the list's internal state private final List<String> items = new CopyOnWriteArrayList<>(); // or synchronize every access void addItem(String s) { items.add(s); } volatile only guarantees visibility of the reference itself being reassigned; it does nothing to make concurrent mutation of the referenced object (like an ArrayList) safe. Use a thread-safe collection or synchronize all access to it.
private static Singleton instance; // NOT volatile static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } private static volatile Singleton instance; // volatile is required here
Without volatile, double-checked locking is broken: a thread could observe a non-null reference to a partially constructed object due to instruction reordering. volatile prevents this specific reordering hazard.
What two guarantees does volatile actually provide?
Visibility (a write to a volatile field by one thread becomes visible to any thread that subsequently reads it) and ordering (the compiler/CPU cannot reorder other operations around the volatile read/write in ways that would break the resulting happens-before relationship).
Why doesn't declaring a counter field volatile make count++ thread-safe?
count++ is a compound read-modify-write operation, not a single atomic step. volatile only guarantees that each individual read and each individual write of the field is visible and properly ordered — it does nothing to prevent two threads from interleaving the read and write steps of count++ and losing an update.
In the classic double-checked-locking singleton pattern, why must the instance field be declared volatile?
Without volatile, a reading thread could, due to legal reordering, observe a non-null reference to the singleton before the constructor's writes to that object's fields have become visible to it — i.e. it could see a partially-constructed object. volatile prevents this reordering hazard by establishing the necessary happens-before edge.
Does volatile ever cause a thread to block waiting for another thread?
No. volatile reads and writes never block — there is no lock acquisition involved. This is part of why it's lighter-weight than synchronized, but also why it cannot provide mutual exclusion.
What special guarantee does volatile add for long and double fields specifically?
It guarantees that individual reads and writes of a volatile long or double are atomic (never 'torn' into two separate 32-bit operations), which the JLS does not guarantee for non-volatile 64-bit fields on all JVM implementations.
Give an example of a correct use case for volatile, and explain why it's correct there.
A boolean shutdown flag checked in a worker thread's loop and set by another thread (e.g. 'volatile boolean running'). It's correct because both the read and the write are single, non-compound operations on the field itself — exactly the case volatile's visibility/ordering guarantees are designed to cover, with no compound invariant to protect.
If a class has two volatile fields, x and y, that must always be updated together, does volatile keep them consistent?
No. volatile provides guarantees per individual field access, not across multiple fields as a combined unit. A thread could observe x updated but y not yet updated (or vice versa) if they are set as two separate statements. Protecting a multi-field invariant requires mutual exclusion, e.g. synchronized or a Lock.
Why does await() reliably terminate here, and what would happen if 'done' were not volatile?
Because 'done' is volatile, Thread B's write (done = true) is guaranteed visible to Thread A's subsequent reads, so the loop eventually observes true and exits, printing 'finished'. Without volatile, the JIT could legally hoist the read of 'done' out of the loop (or cache it in a register), and Thread A might spin forever, never observing Thread B's write — a classic visibility bug that isn't guaranteed to manifest but is legal under the memory model.
Write a class with a volatile long field and explain what guarantee volatile adds beyond a plain long field in this case.
class Sequence { private volatile long lastId = 0L; void publish(long id) { lastId = id; // single write, becomes visible to other threads } long readLast() { return lastId; // guaranteed to see the latest published value, and not a "torn" half-write } }
Beyond ordinary visibility, volatile guarantees that reads/writes of this 64-bit long field are atomic — no thread can observe a 'torn' value assembled from half of one write and half of another, which the JLS does not guarantee for a non-volatile long/double on every JVM implementation.
Show why replacing a volatile counter's increment with AtomicInteger fixes a lost-update bug, contrasting the two versions.
Version A's count++ still decomposes into read, add, write — volatile only guarantees each of those sub-steps is visible, not that the whole sequence is atomic, so two threads can interleave and lose an increment. Version B's incrementAndGet() performs the read-modify-write as a single atomic hardware-backed operation (typically compare-and-swap under the hood), so no interleaving can cause a lost update; every call's effect is fully applied before another thread's call can observe or act on the field.
class Flag {
private volatile boolean done = false;
void finish() { done = true; }
void await() {
while (!done) { }
System.out.println("finished");
}
}
// Thread A calls await(); Thread B calls finish() shortly after.// Version A (buggy): private volatile int count = 0; void inc() { count++; }
// Version B (fixed): private final AtomicInteger count = new AtomicInteger(); void inc() { count.incrementAndGet(); }