A learning path ready to make your own.

Java Volatile

TL;DR volatile is a lightweight Java concurrency primitive that guarantees visibility and ordering (release/acquire) but not mutual exclusion. A write to a volatile variable happens-before every subsequent read of that same variable, ensuring the reader sees the write and prior memory actions. Use volatile for simple flags, safe publication of immutable objects, correct double-checked locking (with volatile), and some lock-free patterns. Do not use it for compound read-modify-write operations — use synchronized or java.util.concurrent.atomic (CAS) instead. Since Java 5 (JSR-133) volatile semantics are well-defined; Java 9+ adds VarHandle for finer-grained memory ordering (acquire/release/opaque). Core semantics Visibility: A volatile write by one thread is visible to subsequent volatile reads by other threads. Ordering (release/acquire): A volatile write has release semantics (prior writes cannot be reordered after it). A volatile read has acquire semantics (subsequent reads/writes cannot be reordered before it). Atomicity: Individual reads/writes of volatile variables are atomic for all primitive types (including long/double) and references. Limitations: volatile does not provide mutual exclusion or atomicity for compound operations (e.g., counter++). Happens-before & ordering (summary) Key JMM concepts: program order, monitor order, volatile order, and the happens-before relation. A volatile write happens-before subsequent volatile reads of that variable; happens-before is transitive. These constraints prevent certain reorderings and ensure visibility of memory actions that precede a volatile write. Practical uses and idioms Stop flag: single-writer, many-reader boolean flags (worker loop termination). Safe publication: publish a fully-constructed immutable object by writing its reference to a volatile field. Double-checked locking: correct when the singleton reference is volatile (prevents seeing a partially constructed object). Sequence/stamp patterns: version counters used with acquire/release ordering to detect concurrent modification (optimistic reads). Not suitable: counters using ++ or other read-modify-write sequences — use AtomicInteger, LongAdder, or synchronized blocks. Examples (condensed) Stop flag: private volatile boolean running = true; void stop() { running = false; } Safe publication of immutable object: private static volatile Config cfg; void set(Config c) { cfg = c; } // publishes fully-initialized cfg Config get() { return cfg; } Volatile vs synchronized vs Atomic* volatile: cheap visibility & ordering, no locking, no atomic compound operations. synchronized: mutual exclusion + acquire/release; good for complex invariants. JVM optimizations often reduce its cost. Atomic classes: CAS-based atomic compound operations; suited for lock-free updates and high-concurrency counters. Choose volatile for simple flags/publication, Atomic* for atomic updates, synchronized for multi-field invariants or critical sections. Java 9+ VarHandle VarHandle exposes finer-grained memory modes: getVolatile/setVolatile, getAcquire/setRelease, getOpaque/setOpaque, and CAS operations. Use VarHandle when you need precise, optimized memory-order semantics beyond plain volatile. JVM ↔ CPU mapping The JVM implements volatile semantics using platform-specific fences/intrinsics. Hardware models differ (x86 TSO is stronger than ARM/POWER), but the JMM abstracts those differences — program against the JMM, not CPU fences. Performance considerations & pitfalls volatile is typically cheaper than synchronized, but still incurs fences and can limit compiler/CPU optimizations. Avoid excessive volatile usage on hot paths without benchmarking (use JMH). Beware false sharing: frequently-updated volatile fields on the same cache line cause cache-line bouncing; consider padding or @Contended. Microbenchmarks can mislead; use JMH and realistic workloads. Tools for testing & debugging JCStress — concurrency testing for JMM behaviors. JMH — accurate microbenchmarking (measure volatile vs synchronized vs atomic). Static analyzers (FindBugs/SpotBugs) and careful stress tests for race detection. Best-practices checklist Use volatile for single-writer/multiple-reader flags, safe publication of immutable objects, and simple release-acquire patterns. Do NOT use volatile for compound read-modify-write operations or invariants spanning multiple fields. Prefer Atomic* or synchronized for atomic updates and multi-field invariants. When building low-level primitives, prefer VarHandle for explicit memory modes (acquire/release/opaque). Avoid publishing partially constructed objects; ensure construction completes before volatile publication. Measure performance (JMH) and mitigate false sharing when necessary. Further reading Java Language Specification — Java Memory Model chapter JSR-133 documentation (JMM changes for Java 5) Java Concurrency in Practice (Goetz et al.) VarHandle Javadoc and JCStress resources Closing summary volatile is a powerful, well-specified tool for visibility and ordering (release/acquire) problems. It is ideal for flags, safe publication, and some lock-free patterns, but it is not a replacement for locks or atomic classes for compound operations. Use the JMM as your guide, test with JCStress/JMH, and consider VarHandle for fine-grained memory control.

Follow the trail that experts already trust.

Resources

Read deeper, connect wider, own the subject.

Deep Article

Java volatile — a deep dive

TL;DR

  • volatile in 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 volatile for 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) — use synchronized, java.util.concurrent atomics, or CAS.
  • Since Java 5 (JSR-133) volatile semantics were strengthened; modern JVMs implement volatile via CPU memory fences appropriate for the platform. Java 9+ adds VarHandle and 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 volatile means (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)
  • volatile vs synchronized vs Atomic*
  • 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 volatile variables are always atomic (for all scalar types).
  • A write to a volatile variable happens-before every subsequent read of that volatile.
  • Execution reordering around volatile accesses 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:

  1. Visibility: A write to a volatile field by one thread is guaranteed to be visible to other threads that subsequently read that volatile variable.
  2. 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 long and double) and references.
  • volatile does NOT provide atomicity for compound operations (e.g., ++, x = x + 1, or check-then-act).
  • volatile does 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 synchronized blocks/monitors.
  • Volatile Order: All accesses to a particular volatile variable 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/double after 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.Unsafe intrinsics or VarHandles.

In practice:

  • Use volatile for simple visibility and release-acquire patterns.
  • Use Atomic* for lock-free atomic updates.
  • Use synchronized for complex ...

Ready to see the full tree?

Clone the preview to open the complete learning structure, practice tools, and generated study materials.