Topic 13.4
volatile and the Java Memory Model
In one line
The Java Memory Model (JMM) says when one thread is guaranteed to see another thread's writes. The answer is "when there's a happens-before relationship", created by locks, volatile variables, Thread.start/join and the concurrent utilities. Without one, a thread may see stale or reordered values.
Think of it like this
Two friends share a whiteboard, but each also has a personal sticky-note pad where they jot quick copies. One friend updates the whiteboard; the other keeps reading their old sticky note and never looks up. A **volatile** variable is like a rule: "for this item, always read and write the whiteboard itself". The JMM is the set of rules for when a sticky note must be refreshed from the board.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Java Memory Model (JMM)
- The rules in the Java language specification that say when one thread must see another thread's writes.
- Visibility
- Whether a value written by one thread can be seen by another.
- Reordering
- The compiler or CPU changing the order of memory operations to run faster, which other threads might notice.
- Happens-before
- A guarantee that one action's effects are visible to, and ordered before, another action.
- volatile
- A field modifier that makes every read see the latest write and stops reordering around it.
- Data race
- Two threads accessing the same variable, at least one writing, with no happens-before order between them.
- Safe publication
- Handing an object to other threads in a way that guarantees they see it fully built.
Step by step
01Why a thread can miss an update
The JIT may hoist a non-volatile field read out of a loop: while (!stop) work(); can become if (!stop) while (true) work(); because, within one thread, nothing in the loop changes stop. Another thread setting stop = true is never noticed and the loop runs forever.
This isn't a bug in the JVM: without a happens-before edge, the JMM allows it. Declaring volatile boolean stop forbids the optimisation.
02The happens-before rules you'll use every day
Program order inside a thread; monitor unlock then lock; volatile write then read; Thread.start; Thread.join; and transitivity (if A hb B and B hb C, then A hb C). The java.util.concurrent classes document their own edges, for example "actions before putting an object into a BlockingQueue happen-before actions after taking it out".
03Publishing data with a volatile flag
Writer: data = 42; ready = true;. Reader: while (!ready) wait a bit; print(data);. Because ready is volatile, the write of data (before the volatile write) is visible after the volatile read sees true.
If ready were not volatile, the reader might loop forever, or see ready == true but still read data == 0, because the writes could become visible in either order.
04volatile is not atomic
volatile int count; count++; from two threads still loses updates: each thread reads the latest value, but the read and the write are separate steps. Use AtomicInteger or a lock for read-modify-write.
Rule of thumb: volatile is for one thread writing and others reading, or for independent writes of a single value. Anything that depends on the current value needs atomics or locks.
05Double-checked locking done right
The lazy singleton idiom checks the field without a lock, then again under the lock. It's only correct if the field is volatile: otherwise another thread can see a non-null reference to an object whose constructor hasn't finished writing its fields.
Simpler alternatives exist: an enum singleton, or the holder-class idiom (a static nested class initialised on first use), which the class-loading rules make thread-safe for free.
class Config {
private static volatile Config instance;
static Config get() {
Config c = instance; // one volatile read on the fast path
if (c == null) {
synchronized (Config.class) {
c = instance;
if (c == null) instance = c = new Config();
}
}
return c;
}
}06final fields and immutable objects
The JMM's final-field rule means an object with only final fields, built by a constructor that doesn't leak this, is seen correctly by every thread even if the reference itself is passed through a plain field. That's why String, records with final components and List.of collections are safe to share.
Try it yourself
- 1
Remove volatile from the flag
In "A volatile stop flag", remove
volatileand theThread.sleep(1)inside the loop, leavingwhile (running) rounds++;. On a desktop JDK the worker may never stop, because the JIT hoists the read. (Don't do this in the browser runner: the program will hit the time limit.) - 2
Print the volatile count
In "volatile doesn't make ++ atomic", print
volatileCountitself. Run it a few times on your own JDK and watch the number change.
Code & diagrams
Expected output
worker saw running = false and stopped
main: worker finishedExpected output
reader sees data = 42On a multi-core JDK the volatile count usually comes out below 100000. Visibility isn't atomicity.
Expected output
atomic: 100000
volatile count is at most 100000: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Double-checked locking without volatile
Remove volatile from the instance field in the double-checked locking snippet.
Myth vs fact
Myth
volatile makes operations on a variable atomic.
Fact
Only single reads and writes. ++, +=, and check-then-act still need atomics or locks.
Myth
Visibility problems only happen on exotic hardware.
Fact
The JIT's own optimisations (like hoisting reads out of loops) cause them on every platform.
Myth
synchronized is only about mutual exclusion.
Fact
It also creates happens-before edges, so it's a visibility tool too.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
volatile accesses compile to plain loads and stores plus memory barriers; on x86 (a strong memory model) a volatile read is almost free and a volatile write needs a
lock-prefixed instruction, while on ARM both need explicit barrier instructions. - ▸
VarHandle(Java 9) exposes finer access modes (plain, opaque, acquire/release, volatile) and compare-and-set, the same machineryjava.util.concurrentuses internally;getAcquire/setReleasegive publication semantics cheaper than full volatile. - ▸
longanddoublewrites that aren't volatile may legally be split into two 32-bit writes (JLS 17.7), so another thread can see half of a value. In practice 64-bit JVMs write them atomically, butvolatileguarantees it.
Remember this
- 1
Modern CPUs and compilers reorder and cache memory operations for speed: values sit in registers and CPU caches, and writes can become visible to other cores in a different order than the code shows. In one thread this is invisible, but across threads it means one thread can see stale values or see writes "out of order".
- 2
The JMM defines happens-before: if action A happens-before action B, then B sees A's effects (and everything before A). Key rules: within one thread, earlier statements happen-before later ones; unlocking a monitor happens-before every later lock of it; a **write to a
volatilefield happens-before every later read of that field;Thread.start()happens-before anything in the started thread; everything in a thread happens-before another thread's successfuljoin()** on it. - 3
**
volatileguarantees visibility and ordering for that one field: reads always see the latest write, and writes before a volatile write can't be moved after it (and reads after a volatile read can't be moved before it). It does not** make compound actions atomic:volatileCount++still loses updates. - 4
Typical correct uses of
volatile: a stop flag written by one thread and read by another; publishing an immutable object reference (write all fields, then assign the volatile reference); and the double-checked locking idiom for lazy singletons. - 5
**
finalfields** get a special guarantee: once a constructor finishes, any thread that obtains a reference to the object (without the reference escaping during construction) sees the final fields' values. That's why immutable objects with final fields are safe to share. - 6
A program with no data races (every shared variable is accessed under a happens-before relation when at least one access is a write) behaves as if threads simply took turns (sequential consistency). Write data-race-free code using locks, volatile, atomics or concurrent collections, and you never need to reason about reordering yourself.
Explain it without notes
What problem does the Java Memory Model solve, and what is happens-before?
What exactly does volatile guarantee, and what doesn't it?
Why does double-checked locking need volatile?
Practice
Write a class that publishes an immutable Settings record to readers through a volatile field and lets a writer replace it.
Trade-offs
- ↔
volatile is cheap and lock-free but only fits single-variable visibility; locks and atomics handle compound updates.
- ↔
Relying on happens-before from concurrent utilities is safest; hand-tuned VarHandle access modes are faster but very easy to get wrong.
Done when you can
Done when you can explain visibility, reordering and happens-before in plain words.
Done when you can list the main happens-before rules (locks, volatile, start, join).
Done when you use volatile for flags and publication, and atomics or locks for read-modify-write.
Done when you can explain safe publication and the final-field guarantee.