Java volatile Keyword
Use the volatile keyword in Java to give field reads and writes visibility and ordering guarantees across threads — without taking a lock.
Java volatile Keyword
synchronized solves two problems at once: mutual exclusion and memory visibility. Sometimes you only need the second. A single boolean flag that one thread sets and another thread polls doesn't need exclusive access — there's only one writer and one or many readers, and the work itself doesn't compose. Locking it is overkill; not locking it is broken. volatile is the keyword that gives you visibility without exclusion.
It's a narrow tool. Get the use case right and it's the fastest thread-safe primitive Java has. Get it wrong — try to use it for compound updates like ++ — and you'll have the same bugs synchronized was supposed to fix.
The problem volatile exists to solve
Without any synchronisation:
class Worker implements Runnable {
boolean stop = false; // plain field
public void run() {
while (!stop) { // hot loop
doWork();
}
}
}
Worker w = new Worker();
new Thread(w).start();
Thread.sleep(1000);
w.stop = true; // ask it to stopThis program may never stop. The reason: nothing in the JVM is required to flush stop from one thread's CPU cache to main memory, and nothing is required to invalidate the worker's cached copy when the main thread writes. The JIT is also allowed to hoist !stop out of the loop entirely — the compiler sees that the body doesn't modify stop, so it caches the value in a register. Forever.
This is the visibility bug. The fix:
volatile boolean stop = false;Now every write to stop flushes to main memory and every read pulls from main memory. The JIT can't hoist the read; the loop sees the new value within microseconds of the writer's update.
What volatile guarantees
Four things:
- Visibility. A write to a
volatilefield is guaranteed to be visible to subsequent reads in any other thread. - Atomicity of read and write — but only for the field itself. A
volatile longandvolatile doubleread/write is atomic; withoutvolatile, a 64-bit read/write may be torn on a 32-bit JVM. - Ordering (happens-before). Everything that happened before a
volatilewrite in the writing thread is visible to anything that happens after the matchingvolatileread in the reading thread. This is the part you don't see in the syntax but is the most powerful guarantee. - No reordering across the access. The compiler and CPU can't move ordinary reads/writes past a
volatileaccess in either direction.
The third one — happens-before via a volatile write/read pair — is sometimes called the cheapest publication primitive. If you write a field, then a volatile boolean ready = true, another thread that sees ready == true is guaranteed to see the earlier write too. That's how a lot of thread-safe initialisation gets done without a lock.
What volatile does not guarantee
The most common mistake:
volatile int counter = 0;
void increment() { counter++; } // STILL BROKENvolatile makes the read atomic and the write atomic — but counter++ is read-modify-write, which is three operations. Two threads both read 42, both compute 43, both write 43. One increment is still lost.
For compound updates, volatile is not enough. Use synchronized, a Lock, or — almost always the right answer — an AtomicInteger.
The other thing volatile doesn't do:
- It doesn't give mutual exclusion. Two threads can write to a
volatilefield simultaneously; the result is "one of them wins, the other is lost," which is the right answer for flag-like state and the wrong answer for "merge these two values." - It doesn't synchronise multiple fields atomically together. If your invariant is "if A is true then B must be 42," writing each one to a separate
volatilefield can leave another thread seeing the new A and the old B.
The publication pattern
The most useful application of volatile outside the stop-flag case:
class LazyResource {
private Resource cached; // not volatile
private volatile boolean ready = false; // the publication flag
public Resource get() {
if (!ready) {
synchronized (this) {
if (!ready) {
cached = buildResource(); // expensive init
ready = true; // publishes cached
}
}
}
return cached;
}
}The volatile write of ready = true publishes the prior write to cached. Any thread that subsequently reads ready == true is guaranteed to see the fully-initialised cached. The lock is only contended on the first call; subsequent calls just read ready and skip the synchronisation entirely. This is the classic double-checked locking idiom, made correct by volatile.
Without volatile on ready, the optimisation is broken — the second thread can see ready == true and then read cached as null. With volatile, it can't.
(The modern alternative is just to do private final Resource cached = buildResource(); if eager init is fine, or Supplier<Resource> lazy = Suppliers.memoize(...) from your library of choice. Hand-rolled double-checked locking is rare in modern code; the pattern is worth knowing because you'll read it.)
When volatile is exactly right
A small handful of use cases where volatile is the best answer:
- A single-writer status flag read by many threads. Stop signals, "ready" flags, configuration version numbers.
- A reference to an immutable value that gets atomically swapped. Cache that the writer rebuilds and publishes; readers see the old or new whole object, never a half-built one.
- Publishing the result of one-time initialisation through the double-checked-locking pattern.
- A timestamp or sequence number that one thread sets and others read — where missing the very latest update is acceptable but the value must always be self-consistent.
Outside those cases, you usually want a higher-level tool. Don't be clever with volatile — its semantics are too thin to support general state.
volatile long and volatile double on 32-bit JVMs
A historical wrinkle. On a 32-bit JVM, a non-volatile long or double read/write is allowed to be split into two 32-bit accesses. Two threads can produce a torn value — the high 32 bits of one write and the low 32 bits of another. volatile long and volatile double are guaranteed atomic regardless of word size.
Modern JVMs almost always run 64-bit, and 64-bit JVMs make all primitives atomic anyway, but the rule is still in the spec. If you have a long/double shared across threads, mark it volatile (or use AtomicLong/AtomicDouble).
A worked example: the stop-flag bug
The program below runs the worker with a plain boolean first (which usually never stops), then with a volatile boolean (which stops promptly). A watchdog cuts the broken case off after 2 seconds so the demo terminates.
What to take from the run:
- The
PLAINworker often did not stop within the watchdog window. The main thread wrotestop = true; the worker thread, with its register-cached copy ofstop, never noticed. Addvolatileand the bug disappears. This is the visibility problem — every multithreaded program has a version of it. - The
VOLATILEworker stopped within a microsecond or two of the write. That's the cost of the memory barrier the JVM emits for avolatilewrite/read pair — single-digit microseconds at worst, and often lower. Cheap for the right use case. - The
VOLATILE++counter was less than200,000.volatiledoes not turnn++into an atomic operation — it's still read-modify-write, and two threads can both read42and both store43. This is the most common misuse ofvolatile. For compound updates, reach forAtomicInteger(next chapter). - The cost of
volatileis small but real — every read and every write touches main memory and emits a memory barrier. For a flag that's read once per loop iteration, that's nothing. For a field that's read in a tight inner loop, the barrier cost adds up. If you find a hotvolatilein a profile, consider whether the read can be hoisted to a local and the loop body run on that snapshot. volatileis also the publication mechanism for a reference — write the data, thenvolatile-write the reference. The reading thread that sees the new reference is guaranteed to see all the data the writer prepared. That's the building block of the double-checked-locking and the immutable-value-swap patterns.
What's next
The next chapter, Java Atomic Variables, introduces AtomicInteger, AtomicLong, AtomicReference, and the rest of the java.util.concurrent.atomic family — the right tool for "increment a counter from many threads" and any other lock-free compound update.
Practice
You declare `private volatile int counter = 0;` and call `counter++` from multiple threads. After 4 threads each do 100,000 increments, what value does `counter` typically hold?