volatile & Memory Visibility
volatile & Memory Visibility
You already know that a race condition occurs when two threads modify shared data without synchronisation. But there is a subtler, and often more treacherous, category of concurrency bug: a thread can read a value that is stale — not because another thread is writing at the same time, but simply because the JVM or the CPU is allowed to keep a private cached copy of the variable. Understanding this is the heart of the Java Memory Model.
The Visibility Problem
Modern CPUs do not read and write main memory on every instruction. Each core has its own registers and one or more layers of cache (L1, L2, L3). The JVM exploits this heavily: a value written by Thread A might sit in A's CPU cache and never be flushed to main memory. Thread B, running on a different core, reads from its own cache and sees the old value indefinitely.
Consider this classic example:
On many JVMs and hardware configurations this program never terminates. The worker thread caches keepRunning in a register and never re-reads main memory. The write by the main thread is invisible to it.
Happens-Before: The Formal Guarantee
The Java Memory Model does not think in terms of caches or CPU instructions. Instead it defines a single, hardware-agnostic rule called happens-before.
A happens-before relationship between two actions (a write and a read) guarantees that the write is visible to the read. Put differently: if action A happens-before action B, then every memory write performed by A (or any action before A) is guaranteed to be visible to B.
Key happens-before rules in the JMM:
- Program order: Within a single thread, each statement happens-before the next.
- Monitor unlock → lock: Unlocking a
synchronizedblock happens-before any subsequent lock on the same monitor. - volatile write → volatile read: A write to a
volatilevariable happens-before every subsequent read of that same variable. - Thread start:
Thread.start()happens-before any action in the started thread. - Thread join: All actions in a thread happen-before
Thread.join()returns.
The volatile Keyword
Declaring a field volatile establishes a happens-before edge between every write to that field and every subsequent read of it, across all threads.
Two concrete effects follow from this:
- Visibility: A write to a
volatilefield is immediately flushed to main memory. A read always fetches from main memory (bypassing the CPU cache). The stale-read problem disappears. - No reordering around volatile access: The JVM and CPU are forbidden from moving reads or writes of other variables across a
volatileaccess. This is the "memory barrier" effect.
Fixing the earlier demo is trivial:
Adding volatile guarantees the worker thread will observe the write within a bounded time.
volatile Is Not a Replacement for synchronized
volatile guarantees visibility but not atomicity. A classic mistake:
Even though every read of count goes to main memory, the compound read-modify-write of count++ is still a race condition: two threads can both read the same value, both add 1, and both write back, losing one increment.
For compound operations you need either synchronized or an AtomicInteger (covered in the next lesson). The correct use cases for volatile are:
- A single boolean flag set by one thread and read by others (e.g., a shutdown flag).
- A single reference (or primitive) that is written by exactly one thread and read by many, where only the latest value matters.
- Fields that act as publication barriers — writing a
volatilefield after constructing an object ensures the constructed object is safely visible to other threads that subsequently read that field.
Publication via volatile
A subtle but important use of volatile is safe publication. Without it, other threads could see a partially constructed object even if the reference was written after the constructor returned — because the JVM can reorder the field stores inside the constructor with the reference assignment.
volatile. Visibility means: once a value is written, readers will see it. Atomicity means: a compound operation (like ++ or a conditional update) completes as a single indivisible step. volatile gives you only the former.
Performance Considerations
A volatile read is cheaper than synchronized but not free. Each read forces a cache-coherence protocol round-trip on modern hardware (an x86 "mfence" or equivalent barrier). In tight loops that read a volatile many millions of times per second the overhead is measurable. The standard pattern is to copy the volatile field into a local variable at the top of the method and work with the local variable inside the loop, re-reading the volatile only when needed:
Summary
volatile is the lightest-weight synchronisation primitive in Java. It solves the visibility problem by establishing a happens-before relationship between writes and reads, preventing threads from operating on stale cached values. It does not provide atomicity for compound operations — that job belongs to synchronized or the java.util.concurrent.atomic package. Use volatile when you have a single writer, or when the only requirement is that the latest value is always visible, with no multi-step invariant to protect.