volatile in Java
volatile gives a single field two guarantees: every read sees the latest write from any thread (visibility), and reads and writes cannot be reordered across the access (ordering). It does not make compound operations like value++ atomic. Use it for status flags, write-once references, and the published reference in double-checked locking.
The two promises in plain English
volatile is a keyword applied to a Java field. It promises two things about that field, and only that field:
- Every read sees the latest write. No stale cached value, no missing the update.
- Writes that happened before it are published, reads that happen after it are subscribed. The volatile write acts like a "publish this point in time" marker. Any thread that reads the volatile and sees the new value also sees everything the writer did before the volatile write.
That's it. Volatile is a publish/subscribe handshake on a single field.
What it does NOT do
volatile does not make value++ atomic. The ++ operator looks like one thing in source code, but under the hood it is three separate steps: load the current value into a register, add one, store the new value back. volatile makes each of the three visible to other threads, but it does not bundle them into one indivisible operation. Two threads racing on the same volatile long count can interleave like this:
Both threads ran count++ to completion. The expected result was 7, the actual result is 6. Visibility was never the problem; what was missing is atomicity of the read-modify-write as a unit. For that, use AtomicLong.incrementAndGet() for low contention or LongAdder.increment() for high contention. Both bundle the three steps into one CAS retry loop that cannot lose updates.
A picture of the publish/subscribe behaviour
The interesting part of volatile is not the field itself; it is what happens to the writes around it. A volatile write acts as a publish edge for every plain write that came before it, and a volatile read acts as a subscribe edge for every plain read that comes after it. Concretely:
If ready is not declared volatile, two things go wrong:
- The reader's
while (!ready) { ... }loop is allowed to read the field once into a register and then never look at memory again. The JIT does this on hot loops as a normal optimization. The result is a loop that runs forever, even after the writer flipsreadytotrue. - Even if the reader does eventually see
ready = true(because the JVM happens not to apply that optimization on this run), there is no guarantee that the reader sees the previous writes toconfig.timeoutandconfig.retries. The writer may have publishedready = truebefore the twoconfigwrites drained out of its store buffer, so the reader observes a stale config alongside a fresh flag.
When ready is declared volatile, both problems go away: the loop re-reads the field every iteration, and the moment the reader sees ready = true, every plain write the writer did before setting the flag is guaranteed to be visible.
When to reach for volatile
There are three classic uses where volatile is exactly right.
| Use case | Pattern | Why volatile fits |
|---|---|---|
| Status flag that one thread sets and another polls | volatile boolean running; as a stop signal for a worker | Single field, one writer, no compound update; visibility is the whole problem |
| Write-once reference (the singleton in double-checked locking) | private static volatile Singleton instance; | The volatile write publishes the constructor's writes to any reader that later sees a non-null reference |
| Hot-swappable immutable object | volatile RoutingTable table; swapped by table = new RoutingTable(...) | The whole structure is replaced atomically; readers always see a complete, fully-constructed copy |
A bonus case worth knowing: on 32-bit JVMs, plain long and double reads and writes can be torn, meaning a reader can pick up the high word from one update and the low word from another. Declaring those fields volatile forces atomicity on each individual read or write. On 64-bit HotSpot this is already the case in practice, but the JLS only guarantees it under volatile, so portable code that runs on 32-bit JVMs needs the keyword.
When NOT to use volatile
There are three cases where volatile is the wrong tool, even though it looks tempting.
| Anti-pattern | Use this instead | Reason |
|---|---|---|
| Counters, hit rates, anything read-modify-write | AtomicLong or LongAdder | ++ is three operations; volatile does not bundle them |
| Multi-field invariants ("A and B must change together") | synchronized or ReentrantLock | Volatile is per-field; mutual exclusion is what protects multi-field consistency |
A field already accessed only inside a synchronized block | Just leave it plain | The lock already provides publish/subscribe for every field it covers; adding volatile is noise |
The third row is the one that gets stacked unnecessarily. synchronized already gives the same publish/subscribe contract that volatile does, plus mutual exclusion. Adding volatile to a field that is only ever accessed under the lock buys nothing.
Why this shows up in every interview
Volatile is the cheapest publish/subscribe primitive in Java, but it's also the most misunderstood. The interview questions are predictable, and they all come back to the same two promises (and what they don't include):
- "Why does this spin loop hang without volatile?" → no visibility, the loop never sees the write.
- "Why is
volatile int counter; counter++;broken?" → volatile doesn't make++atomic. - "Why is volatile required in double-checked locking?" → without it, the constructor's writes can be reordered after the assignment, so a reader can see a non-null reference to a half-built object.
Same answer to all three: volatile provides publish/subscribe on one field. Anything more (atomicity on compound updates, mutual exclusion, multi-field consistency) needs another tool.
Primitives by language
- volatile primitive fields
- volatile object references
- VarHandle for finer-grained acquire/release control
Implementation
Without volatile, the worker thread might cache running in a register and never see the change. The JIT is allowed to hoist non-volatile reads out of loops. With volatile, every read goes through main memory.
1 class Worker implements Runnable {
2 private volatile boolean running = true;
3
4 public void stop() { running = false; }
5
6 @Override
7 public void run() {
8 while (running) {
9 doSomeWork();
10 }
11 }
12 }Pre-Java-5 DCL was famously broken because the constructor's writes could be reordered with the assignment of the reference. Volatile on the field fixes it: writes that happened before the volatile write are visible to any reader that sees a non-null reference. Use this or, simpler, the static-holder idiom.
1 class ConfigService {
2 private static volatile ConfigService instance;
3
4 public static ConfigService get() {
5 ConfigService local = instance; // single read
6 if (local == null) {
7 synchronized (ConfigService.class) {
8 local = instance;
9 if (local == null) {
10 local = new ConfigService(); // constructor runs
11 instance = local; // volatile write: publishes
12 }
13 }
14 }
15 return local;
16 }
17 }The ++ is three operations: load, add, store. Two threads can both load the same value, both add one, both store. One increment is lost. Volatile makes each load and each store visible, but it does not make the three together atomic.
1 class Counter {
2 private volatile long count; // BROKEN for inc()
3 public void inc() { count++; } // load, add 1, store: not atomic
4 }
5
6 // Correct: AtomicLong handles the read-modify-write atomically
7 class CounterFixed {
8 private final AtomicLong count = new AtomicLong();
9 public void inc() { count.incrementAndGet(); }
10 }Volatile reference is the cheapest way to publish an immutable object. Build it, set the volatile, all readers see the fully-constructed value. Pattern used by configuration reload, hot-swappable feature flags, dynamic routing tables.
1 public final class RoutingTable {
2 private final Map<String, String> routes;
3 public RoutingTable(Map<String, String> routes) {
4 this.routes = Map.copyOf(routes); // immutable copy
5 }
6 public String routeFor(String key) { return routes.get(key); }
7 }
8
9 class Router {
10 private volatile RoutingTable table; // hot-swappable
11
12 public void install(Map<String, String> newRoutes) {
13 table = new RoutingTable(newRoutes); // one volatile write
14 }
15 public String route(String key) {
16 return table.routeFor(key); // lock-free read
17 }
18 }Key points
- •Visibility: a write to a volatile field is immediately visible to any thread that reads it.
- •Ordering: writes that happened before the volatile write are visible to a reader that observes it. Reads after the volatile read see those writes too.
- •NOT atomic for compound operations. value++ is read-modify-write, not one operation.
- •Long and double on 32-bit JVMs are atomic only when declared volatile.
- •For atomicity, use AtomicInteger / AtomicLong / AtomicReference. When both are needed, those classes already provide volatile semantics.
Follow-up questions
▸Is volatile a substitute for synchronized?
▸What is the cost of volatile?
▸Does volatile help with long and double on 32-bit JVMs?
▸When prefer VarHandle over volatile?
Gotchas
- !volatile does not lock; multiple writers can still race on the same field
- !volatile array reference is volatile, but reading array[i] is NOT (the element access is plain). Use AtomicReferenceArray for per-element ordering
- !Long and double need volatile to be atomic on 32-bit JVMs
- !Compound updates (value++, value = compute(value)) are not atomic under volatile alone
- !synchronized provides everything volatile does, plus mutual exclusion. Do not stack them on the same field