Visibility & volatile in Java
Caching across cores; volatile guarantees visibility but not atomicity.
Whiteboard vs notebooks
Picture a shared whiteboard (main memory) and workers who copy values into private notebooks (CPU caches and registers). Without synchronization, a write by one thread may sit in its notebook and never become visible to another thread. Nothing forces the copy back to the whiteboard.
The compiler makes it worse
The JIT sees that the loop body never changes stop, so it may read the field once and hoist it out of the loop. That's legal, because nothing creates a happens-before edge between main's write and the worker's reads. Result: an infinite loop.
while (!stop) { }
// the JIT may turn it into:
if (!stop) {
while (true) { }
}volatile: always use the whiteboard
Declare the field volatile and every read sees the most recent write, with ordering guarantees around it. That's ideal for a flag that one thread writes and others read.
private volatile boolean running = true;
void stop() { running = false; }
void loop() {
while (running) { work(); }
}volatile is not atomic
volatile gives visibility and ordering, not atomicity. With volatile long requests and many threads doing requests++, totals still come out too low: two threads can both read the same value. Making it static, adding a backup field or calling yield() won't help. Use AtomicLong or LongAdder, or a lock.
Your turn
What does this print?
class Settings {
static volatile final String MODE = "fast";
}
void main() {
System.out.println(Settings.MODE);
}fastCompile errornull
Show the answer
Compile error. A field can't be both final and volatile. A final field never changes after construction, so "see the latest write" would be meaningless, and the compiler rejects the combination.
Pick the lightest tool
volatile: visibility and ordering only. synchronized: mutual exclusion plus visibility. AtomicInteger: lock-free atomic updates. final field: safely visible to all threads once construction ends. A stop flag needs volatile; a counter needs atomicity.
Works on my machine
A stop flag without volatile often works in development, then hangs in production. Why? The loop only gets JIT-compiled (and the read hoisted) after it has run many times, under real load. Bugs that depend on compiler optimizations look random, so the rule is simple: cross-thread flags are volatile.
Key takeaways
- volatile = visibility + ordering, not atomicity
- Ideal for a flag written by one thread and read by others
- A loop on a plain (non-volatile) flag may spin forever
- For counters, use AtomicInteger or a lock
On x86 CPUs a volatile read compiles to an ordinary load; the cost of volatile is paid on writes, which need a memory fence.
Practice questions
A worker loops while (!stop) {} where stop is a plain boolean field. Main sets stop = true, but in production the worker spins forever. Why?
- Without volatile or locking, the JIT may hoist the read, so the worker never sees the update
- Main's write threw an exception that was swallowed
- boolean writes are not atomic in Java
- The worker has a higher priority than main
Check your answer
Without volatile or locking, the JIT may hoist the read, so the worker never sees the update. Nothing creates a happens-before edge between main's write and the worker's reads, so the JIT may read the field once and loop forever. Declaring stop volatile fixes it.
What does this print?
class Config {
volatile final int limit = 10;
}
void main() {
System.out.println(new Config().limit);
}- Compile error
- 10
- 0
- Throws IllegalStateException
Check your answer
Compile error. A field can't be both final and volatile. final fields never change after construction, so volatile would be meaningless — the compiler rejects the combination.