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
- set x = 42;
- flag = true; // volatile write
Thread 2:
- if (flag) // volatile read
- 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:
1class Worker implements Runnable {
2 private volatile boolean running = true;
3
4 public void run() {
5 while (running) {
6 // do work
7 }
8 }
9
10 public void stop() {
11 running = false; // volatile write
12 }
13}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:
1class Holder {
2 private static volatile MyImmutable obj;
3
4 static void init() {
5 MyImmutable tmp = new MyImmutable(...); // fully constructed
6 obj = tmp; // volatile write publishes the reference and prevents reordering
7 }
8
9 static MyImmutable get() {
10 return obj; // volatile read
11 }
12}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:
1class Singleton {
2 private static volatile Singleton instance;
3
4 static Singleton getInstance() {
5 Singleton result = instance;
6 if (result == null) {
7 synchronized (Singleton.class) {
8 result = instance;
9 if (result == null) {
10 result = new Singleton();
11 instance = result; // volatile write
12 }
13 }
14 }
15 return result;
16 }
17}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:
1class VersionedContainer {
2 private volatile long version;
3 private Data data;
4
5 // writer:
6 void update(Data newData) {
7 version++; // make version odd (or use release semantics)
8 data = newData; // write data
9 version++; // make version even
10 }
11
12 // optimistic reader:
13 Data read() {
14 while (true) {
15 long v1 = version; // volatile read (acquire)
16 Data d = data; // normal read
17 long v2 = version; // volatile read
18 if (v1 == v2 && (v1 & 1) == 0) return d; // no concurrent write
19 }
20 }
21}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:
1class Counter {
2 private volatile int counter = 0;
3
4 void increment() {
5 counter++; // Read-modify-write: still races, lost increments possible
6 }
7}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 critical sections and invariants spanning multiple variables.
Java 9+ VarHandle (and memory modes)
Java 9 introduced VarHandle (java.lang.invoke.VarHandle) and standardized lower-level memory-access operations with different memory-ordering modes:
- getVolatile / setVolatile: full volatile read/write (acquire/release + volatile ordering)
- getAcquire / setRelease: acquire and release semantics (weaker than full volatile in some aspects)
- getOpaque / setOpaque: weak ordering, minimal ordering (opaque)
- compareAndSet / compareAndExchange / getAndSet: atomic CAS operations
VarHandle allows finer-grained control:
1import java.lang.invoke.MethodHandles;
2import java.lang.invoke.VarHandle;
3
4class Example {
5 private volatile int x; // still works
6 private static final VarHandle XHANDLE;
7
8 static {
9 try {
10 XHANDLE = MethodHandles.lookup()
11 .findVarHandle(Example.class, "x", int.class);
12 } catch (Exception e) {
13 throw new Error(e);
14 }
15 }
16
17 void setAcquire(int v) {
18 XHANDLE.setRelease(this, v); // release semantics
19 }
20
21 int getAcquire() {
22 return (int) XHANDLE.getAcquire(this); // acquire semantics
23 }
24
25 void setOpaque(int v) {
26 XHANDLE.setOpaque(this, v); // minimal ordering
27 }
28}Functions:
- setRelease / getAcquire implement release/acquire semantics (useful for implementing patterns that don't need full volatile).
- setOpaque / getOpaque provide even weaker guarantees (for optimizations).
- VarHandle supports
compareAndSetandcompareAndExchange*for atomic updates.
VarHandle makes it possible to express more performance-tuned concurrency algorithms whose correctness needs specific ordering.
JVM and CPU mapping: fences and memory barriers
The JVM implements volatile semantics with memory fence instructions appropriate to the platform. The Java programmer reason about JMM, not about CPU fences directly, but it's useful to understand mapping.
- x86 (TSO): Stronger memory model than many CPUs. It prevents most reorders except store->load (a store followed by a subsequent load to a different location can be reordered). On x86, volatile writes often map to a store with an implicit memory fence (e.g., a locked instruction or
mfence) or uselockprefix on a trivial instruction; volatile reads may map to a load with appropriate fence. - ARM/ARM64/POWER: Weaker memory models that allow many reorderings; JVM must emit explicit acquire/release fences (dmb/ldar/stlr, etc.) or full fences where needed.
- HotSpot implements volatile via platform-specific intrinsics and memory fence sequences; exact implementation details evolve.
The programmer should rely on the JMM rather than architecture-specific details — JMM abstracts hardware behavior.
Performance considerations and pitfalls
- Cost:
volatileis typically cheaper thansynchronizedwhen used properly. But volatile writes/read still require memory barriers and can prevent certain compiler and CPU optimizations. Excessive use can degrade performance on critical hot paths. - False sharing: placing frequently-updated volatile variables on the same cache line as other data may cause cache-line bouncing and degrade performance. Padding or
@Contended(JDK 8+) can mitigate false sharing. - Microbenchmark pitfall: improper benchmarking (JMH recommended) can mislead about volatile costs because of JVM optimizations, inlining, etc.
- Escape analysis and scalar replacement: sometimes
synchronizedoverhead can be eliminated by JIT via escape analysis; volatile semantics are harder to eliminate because they must be observed. - Use of
volatilefor complex invariants is unsafe: even if you make several fields volatile, invariants that involve multiple fields simultaneously generally require atomicity guaranteed by locks or atomic updates.
Advanced examples and corner cases
1) Reordering example (allowed vs not allowed)
Consider:
Thread 1:
- a = 1; // normal write
- v = true; // volatile write
Thread 2:
- if (v) {
- int x = a; // normal read; guaranteed to see 1 }
Because the volatile write and read create a happens-before edge, the read of a cannot be moved before the volatile read; hence x will see 1.
Conversely, if you had: Thread 1:
- a = 1; // normal write
Thread 2:
- int x = a; // normal read There is no guarantee of visibility.
2) Immutable object safe publication nuance
If the publisher stores a this reference or a reference to an object into a volatile field only after full construction finishes, readers will see fully constructed state. But avoid publishing partially-constructed objects.
Bad:
1class Node {
2 Node next;
3 public Node() {
4 // starts a thread that accesses 'this' before constructor ends — dangerous
5 }
6}Good: set volatile reference only after construction completes.
3) Volatile read doesn't automatically protect other variables (unless happens-before established)
Only actions that happen-before the volatile write are guaranteed visible to the reader after the subsequent volatile read. Ordering matters.
Tools, testing and debugging concurrency
- JCStress (OpenJDK) — a specialized concurrency testing tool to detect JMM-related bugs and surprising interleavings. Great for testing small concurrency kernels.
- ThreadDump/stack traces for deadlocks; but volatile-related bugs are often data races, not deadlocks.
- FindBugs / SpotBugs and IDE warnings sometimes pick up suspicious non-volatile shared fields or check-then-act patterns.
- JMH — microbenchmark harness for benchmarking volatile vs synchronized vs atomic performance correctly.
- Logging and reproducibility: concurrency bugs are often timing-dependent; reproduce via stress tests, randomness, timeouts.
Best practices checklist
-
Use
volatilefor:- Single-writer-multiple-reader flags or state fields.
- Safe publication of immutable or properly constructed objects.
- Simple release-acquire synchronization patterns.
- Lightweight coordination where mutual exclusion is unnecessary.
-
Do not use
volatilefor:- Incrementing counters and other read-modify-write operations (use
Atomic*orsynchronized). - Protecting invariants involving multiple fields (use
synchronized).
- Incrementing counters and other read-modify-write operations (use
-
Prefer
AtomicInteger,AtomicReference, orLongAdderfor atomic updates / high-concurrency counters. -
When writing low-level concurrency primitives, prefer
VarHandle(Java 9+) for fine-grained memory ordering. -
Avoid publishing partially constructed objects. Ensure that final fields or volatile write-based publication occur only after construction completes.
-
Avoid overusing
volatileon performance-critical hot paths without measurement (use JMH). -
Beware of false sharing; consider padding or
@Contendedif necessary.
Example code snippets
- Correct safe publication:
1final class Config {
2 final int a;
3 final String b;
4
5 Config(int a, String b) {
6 this.a = a;
7 this.b = b;
8 }
9}
10
11class Provider {
12 private static volatile Config cfg;
13
14 static void setConfig(Config newCfg) {
15 cfg = newCfg; // volatile write
16 }
17
18 static Config getConfig() {
19 return cfg; // volatile read -> guaranteed to see fully initialized Config
20 }
21}- Broken counter (and fix):
Broken:
1class Counter {
2 private volatile int c = 0;
3 void inc() { c++; } // not atomic
4}Fixed with AtomicInteger:
1import java.util.concurrent.atomic.AtomicInteger;
2class Counter {
3 private final AtomicInteger c = new AtomicInteger(0);
4 void inc() { c.incrementAndGet(); }
5}-
Double-check locking singleton (correct with volatile): (see earlier code sample)
-
VarHandle acquire/release example:
1import java.lang.invoke.MethodHandles;
2import java.lang.invoke.VarHandle;
3
4class Sling {
5 private int value;
6 private static final VarHandle VALUE;
7
8 static {
9 try {
10 VALUE = MethodHandles.lookup().findVarHandle(Sling.class, "value", int.class);
11 } catch (Exception e) {
12 throw new Error(e);
13 }
14 }
15
16 void setRelease(int v) {
17 VALUE.setRelease(this, v); // release
18 }
19
20 int getAcquire() {
21 return (int) VALUE.getAcquire(this); // acquire
22 }
23}Current state and future implications
volatilesemantics are stable and well specified by the JMM since Java 5 (JSR-133).- Java 9's
VarHandlegives developers fine-grained control over the memory ordering. This enables development of highly-tuned lock-free algorithms with explicit semantics (acquire/release/opaque). - The trend is to expose more low-level primitives (VarHandle, Unsafe) in controlled ways so concurrency libraries can express precise memory behavior without relying only on
volatileorsynchronized. - Projects such as Project Loom (fibers/virtual threads) do not change the JMM semantics for
volatile, but may shift how threads are used and increase importance of scalable concurrency primitives (e.g.,LongAdder, lock-free algorithms). - Hardware continues to evolve; JMM provides portability: you can reason at Java-level and let JVM emit correct fences for the hardware.
Further reading and references
- Java Language Specification (JLS): Chapter on the Java Memory Model — authoritative reference.
- JSR-133 FAQ and documentation — specification of memory model changes for Java 5.
- "Java Concurrency in Practice" — Brian Goetz et al. (practical patterns and guidance).
- JCStress — OpenJDK tool for concurrency testing.
- VarHandle Javadoc (java.lang.invoke.VarHandle).
- Articles and papers on memory models (e.g., "Memory Models: a Case for Rethinking", or papers by Bill Pugh & Jeremy Manson on JMM).
Summary / Closing
volatile is a fundamental tool in Java concurrency: simple, cheap (relative to synchronization), and powerful when used for visibility and ordering (release-acquire). It fixes very specific problems (flags, safe publication, DCL) but is not a replacement for locks or atomic classes for compound actions and complex invariants. Use the JMM as your mental model, leverage Atomic* and VarHandle when needed, test with jcstress/JMH, and follow the rules described here to avoid subtle concurrency bugs.