Java volatile — a deep dive
TL;DR
volatilein Java is a lightweight concurrency mechanism that provides visibility and ordering guarantees but not mutual exclusion.- A write to a volatile variable happens-before every subsequent read of that same variable; this guarantees visibility of the write and ordering of other memory actions relative to those volatile accesses.
- Use
volatilefor simple state flags, safe publication of immutable objects, some lazy initialization patterns (when used properly), and certain lock-free algorithms. Do not use it for compound actions (read-modify-write) — usesynchronized,java.util.concurrentatomics, or CAS. - Since Java 5 (JSR-133)
volatilesemantics were strengthened; modern JVMs implementvolatilevia CPU memory fences appropriate for the platform. Java 9+ addsVarHandleand richer memory-ordering operations (acquire/release/opaque).
This article explains the history, theory (Java Memory Model), practical patterns, pitfalls, JVM/hardware mapping, modern alternatives (VarHandle, Atomic*), performance considerations, examples and recommended practice.
Contents
- History and background
- What
volatilemeans (semantics) - Happens-before, synchronization order, and ordering guarantees
- Atomicity vs visibility vs ordering
- Practical uses and idioms
- stop flag
- safe publication of immutable objects
- double-checked locking (corrected)
- incorrect uses (counter example)
volatilevssynchronizedvsAtomic*- Java 9+ VarHandle and fine-grained memory modes
- Mapping to CPU memory models and JVM fences
- Performance considerations and pitfalls (false sharing, microbenchmarks)
- Tools, testing and debugging concurrency
- Best-practices checklist
- Further reading & references
History and background
Before Java 5 the Java Memory Model (JMM) was vague and allowed surprising behaviors when sharing mutable data between threads. In 2004 the JSR-133 effort revised and clarified the JMM for Java 5 and later, defining precise happens-before relationships and strengthening volatile semantics. After JSR-133:
- Reads/writes of
volatilevariables are always atomic (for all scalar types). - A write to a
volatilevariable happens-before every subsequent read of that volatile. - Execution reordering around
volatileaccesses is constrained to provide visibility and ordering guarantees.
The change fixed classic concurrency bugs such as broken double-checked locking and incorrect safe publication.
What volatile means (semantics)
In Java, declaring a field volatile provides two primary guarantees:
- Visibility: A write to a volatile field by one thread is guaranteed to be visible to other threads that subsequently read that volatile variable.
- Ordering: Volatile reads/writes constrain reordering. Specifically:
- A volatile write has "release semantics": memory writes before the volatile write in program order cannot be reordered to occur after the volatile write.
- A volatile read has "acquire semantics": memory reads and writes after a volatile read cannot be reordered to occur before the volatile read.
- A volatile write happens-before a subsequent volatile read of the same variable; the reading thread is guaranteed to see the effects of actions that happened-before the volatile write.
Important specifics:
- Accesses (reads and writes) to a volatile variable are atomic for all primitive types (including
longanddouble) and references. volatiledoes NOT provide atomicity for compound operations (e.g., ++,x = x + 1, or check-then-act).volatiledoes NOT provide mutual exclusion: two threads can concurrently write, causing races on higher-level invariants not protected by volatile semantics.
Note: Historically (pre-Java 5), long and double were not guaranteed to be atomic; JSR-133 fixed that.
Happens-before and ordering (formalized)
Key JMM concepts relevant for volatile:
- Program Order: Within a single thread, actions appear in program order.
- Monitor Synchronization Order: The ordering induced by
synchronizedblocks/monitors. - Volatile Order: All accesses to a particular
volatilevariable form a total order consistent with the program order. - Happens-before: A partial order. If action A happens-before B, then all memory writes by A are visible to B.
Crucial volatile rules:
- A write to a volatile field happens-before every subsequent read of that field (in volatile order).
- If A happens-before B and B happens-before C, then A happens-before C (transitivity).
Reordering constraints (intuitively):
- Memory actions before a volatile write cannot be moved after it.
- Memory actions after a volatile read cannot be moved before it.
- This provides a lightweight “release-acquire” synchronization pair.
Example: Thread 1 does 1) set x = 42; 2) flag = true; // volatile write
Thread 2: 1) if (flag) // volatile read 2) read x -> guaranteed to see 42 (or later value). The volatile write -> read establishes happens-before and forces visibility.
Atomicity vs visibility vs ordering — what volatile gives and doesn't
- Atomicity of single reads/writes: Yes (including
long/doubleafter Java 5). - Visibility of those writes to other threads: Yes (when reading the volatile or after happens-before).
- Ordering with respect to other memory operations: Yes, only in the release/acquire sense described above.
- Mutual exclusion: No.
- Atomic compound operations: No.
Consequences: volatile is safe for single-writer-multiple-reader flags, status indicators, and safe publication of immutable objects; but it is not enough to make non-atomic compound updates (like increment) correct.
Practical uses and idioms
1) Stop flag (canonical example)
A typical example where volatile is enough:
```java class Worker implements Runnable { private volatile boolean running = true;
public void run() { while (running) { // do work } }
public void stop() { running = false; // volatile write } } ```
Because running is volatile, when another thread sets running = false, the worker thread will see the change promptly.
2) Safe publication of immutable objects
Volatile can be used to safely publish a reference to an immutable object:
```java class Holder { private static volatile MyImmutable obj;
static void init() { MyImmutable tmp = new MyImmutable(...); // fully constructed obj = tmp; // volatile write publishes the reference and prevents reordering }
static MyImmutable get() { return obj; // volatile read } } ```
Because the write to obj is volatile, any thread that reads the reference after it was set will see a fully initialized MyImmutable (assuming the object is immutable and properly constructed before assignment).
3) Double-checked locking (the corrected pattern)
Before JSR-133 double-checked locking was broken. With volatile it is correct:
```java class Singleton { private static volatile Singleton instance;
static Singleton getInstance() { Singleton result = instance; if (result == null) { synchronized (Singleton.class) { result = instance; if (result == null) { result = new Singleton(); instance = result; // volatile write } } } return result; } } ```
The volatile instance prevents reordering that could cause other threads to see a partially constructed object.
4) Sequence number / Stamped checks
Some lock-free algorithms use a volatile sequence or stamp to detect concurrent modification, e.g., optimistic read:
```java class VersionedContainer { private volatile long version; private Data data;
// writer: void update(Data newData) { version++; // make version odd (or use release semantics) data = newData; // write data version++; // make version even }
// optimistic reader: Data read() { while (true) { long v1 = version; // volatile read (acquire) Data d = data; // normal read long v2 = version; // volatile read if (v1 == v2 && (v1 & 1) == 0) return d; // no concurrent write } } } ```
This is similar to a sequence lock pattern — here volatile provides the necessary ordering for version reads/writes to detect modification. Implementation details matter; this is an advanced pattern.
5) Wrong pattern: volatile counter increment (NOT safe)
Volatile does not make compound updates atomic:
```java class Counter { private volatile int counter = 0;
void increment() { counter++; // Read-modify-write: still races, lost increments possible } } ```
counter++ is a read, increment, write sequence. Use AtomicInteger.incrementAndGet() or synchronized instead.
Volatile vs synchronized vs Atomic classes
- Volatile
- Good for: visibility, safe publication of immutable objects, simple flags, release-acquire ordering.
- Cheap in many cases (no monitor, but JVM uses fences).
- Not for: compound operations, mutual exclusion.
- Synchronized (monitor)
- Good for: mutual exclusion, complex invariants, compound operations.
- Stronger guarantee: entering a monitor has an acquire memory effect and exiting has a release effect, and monitors establish mutual exclusion.
- Slightly heavier but JVM optimizations (biased locking, lock elision, escape analysis) often reduce overhead.
- Atomic classes (java.util.concurrent.atomic)
- Good for: atomic compound operations via CAS (compare-and-set), often provides high performance non-blocking algorithms.
- Provide methods with different memory semantics: get, set, lazySet, compareAndSet, getAndIncrement, etc.
- Implemented with
sun.misc.Unsafeintrinsics or VarHandles.
In practice:
- Use
volatilefor simple visibility and release-acquire patterns. - Use
Atomic*for lock-free atomic updates. - Use
synchronizedfor complex ...