1443 words
7 minutes
Java Volatile Explained: Visibility, Ordering, and Memory Barriers

Java’s volatile keyword is sometimes described as a lighter-weight form of synchronized. That comparison is useful as a starting point, but it can also hide the most important distinction:

  • volatile provides visibility and ordering guarantees.
  • synchronized provides visibility and ordering plus mutual exclusion, which can make a multi-step operation atomic.

Understanding that boundary is the key to using volatile correctly.

What Locks Guarantee#

A lock provides two properties that matter when threads share mutable state:

  1. Mutual exclusion: only the thread holding the lock can execute the protected critical section.
  2. Visibility: changes made before a thread releases a lock become visible to a thread that subsequently acquires the same lock.

Because a lock protects an entire critical section, it can preserve invariants involving multiple fields or multiple operations.

volatile does not create a critical section. Multiple threads can still read and write the field concurrently. Its role is to establish communication between those threads.

What volatile Guarantees#

Declaring a field volatile gives it two central guarantees under the Java Memory Model (JMM).

1. Visibility#

When one thread writes to a volatile field, another thread that later reads that field is guaranteed to observe the write.

More precisely, a write to a volatile field happens-before every subsequent read of that same field. This relationship also publishes earlier ordinary writes made by the writer:

class SharedState {
private int result;
private volatile boolean ready;
void produce() {
result = 42;
ready = true;
}
void consume() {
if (ready) {
System.out.println(result); // guaranteed to see 42
}
}
}

If consume() observes ready == true, it must also observe the result = 42 write that occurred before the volatile write.

Descriptions such as “flush the value to main memory” and “reload it instead of using a CPU cache” are common teaching shortcuts. The JMM does not require one specific cache operation. It defines the observable ordering and visibility guarantees, and the JVM maps them to compiler constraints and machine instructions appropriate for the target architecture.

2. Ordering#

Compilers, the JVM, and processors may reorder operations to improve performance, as long as the change does not alter behavior observable within a correctly synchronized single-threaded execution.

For example:

int value = 0;
boolean ready = false;
value = 42;
ready = true;

Within one thread, swapping the last two independent assignments may appear harmless. Across threads, however, another thread could observe ready == true before it observes the new value.

Making ready volatile prevents reorderings that would violate volatile’s happens-before guarantee. Operations before the volatile write must be published before that write becomes observable, and operations after a volatile read cannot be moved before it in a way that breaks the JMM rules.

Why Reordering Is Usually Safe#

Optimizers preserve data dependencies. Consider this code:

double pi = 3.14; // A
double radius = 1.0; // B
double area = pi * radius * radius; // C

Operation C depends on A and B, so it cannot be evaluated before their values are available. A and B do not depend on each other, so an optimizer may execute them in either order.

This freedom improves instruction-level parallelism and is invisible to the executing thread. It becomes important when another thread observes intermediate shared state. Java therefore requires programs to establish happens-before relationships through constructs such as volatile, locks, thread start and termination, or higher-level concurrency utilities.

Memory Barriers#

A JVM can implement JMM ordering rules with compiler barriers and CPU memory fences. A common conceptual vocabulary describes four barrier relationships:

BarrierConceptual ordering guarantee
LoadLoadReads before the barrier complete before later reads.
StoreStoreWrites before the barrier become ordered before later writes.
LoadStoreEarlier reads are ordered before later writes.
StoreLoadEarlier writes are ordered before later reads.

StoreLoad is generally the strongest and most expensive of these relationships.

Explanations of volatile often use a conservative barrier recipe: ordering ordinary stores before a volatile write, ordering later loads after a volatile read, and using stronger fencing where necessary. Treat that as a model for reasoning, not as a literal instruction sequence emitted by every JVM. On x86, ARM, and other architectures, HotSpot may use different instructions or no explicit fence for some operations because each processor already guarantees certain orderings.

The portable rule is the JMM contract: volatile accesses must produce the specified happens-before behavior regardless of the machine running the program.

What volatile Does Not Guarantee#

volatile does not make a compound action atomic.

class Counter {
private volatile int count;
void increment() {
count++;
}
}

Although each read and write of count is atomic and visible, count++ contains three logical steps:

  1. Read count.
  2. Add one.
  3. Write the result.

Two threads can read the same old value and both write the same new value, losing one increment. Use AtomicInteger or a lock instead:

private final AtomicInteger count = new AtomicInteger();
void increment() {
count.incrementAndGet();
}

The same warning applies to check-then-act logic and invariants spanning multiple fields. Visibility alone cannot prevent other threads from changing state between steps.

Good Uses for volatile#

volatile works best when one write can independently replace the entire state being communicated.

A cancellation or shutdown flag#

class Worker implements Runnable {
private volatile boolean running = true;
void stop() {
running = false;
}
@Override
public void run() {
while (running) {
doWork();
}
}
}

Publishing an immutable snapshot#

record Configuration(String endpoint, int timeoutMillis) {}
class ConfigurationStore {
private volatile Configuration current =
new Configuration("https://example.com", 1_000);
Configuration get() {
return current;
}
void update(Configuration next) {
current = next;
}
}

The reference replacement is atomic, and the volatile write safely publishes the immutable object to readers.

When to Use Something Else#

Choose the tool based on the operation rather than on a vague idea that one primitive is faster:

RequirementAppropriate tool
Publish a flag or immutable snapshotvolatile
Atomically update one valueAtomicInteger, AtomicLong, or AtomicReference
Protect a multi-step invariantsynchronized or Lock
Coordinate queues, latches, or tasksA higher-level java.util.concurrent utility

Performance#

A volatile access is usually cheaper than acquiring a contended lock because it does not provide mutual exclusion or park waiting threads. It is not free: it restricts compiler and processor optimizations and can generate cache-coherence traffic. The exact cost depends on the hardware, JVM, contention pattern, and surrounding code.

Performance is therefore secondary to semantics. If an operation requires atomicity, replacing a lock with volatile is not an optimization; it is a correctness bug.

Summary#

volatile is a communication mechanism for shared fields:

  • A volatile write happens-before a subsequent read of the same field.
  • That relationship makes earlier writes visible to the reading thread.
  • The JVM prevents reorderings that would violate this guarantee.
  • Volatile reads and writes do not make compound operations such as count++ atomic.
  • Exact barriers and assembly instructions are JVM- and architecture-dependent.

Use volatile for independently replaceable state such as flags and immutable snapshots. Use atomic classes or locks when correctness depends on a read-modify-write sequence or an invariant involving multiple pieces of state.

Practical Decision Guide#

When to Use volatile#

Use volatile when threads need to communicate through a single value and no update must be coordinated with another read or write.

Status flags#

A volatile flag can announce that a significant state transition has occurred, such as a shutdown request, completed initialization, or finished task:

private volatile boolean running = true;
void stop() {
running = false;
}
void runLoop() {
while (running) {
doWork();
}
}

One thread writes false, and the worker is guaranteed to observe the change when it next reads running.

Independent observations#

A volatile field can publish an independently replaceable observation that other threads need to read, such as a timestamp, health status, or progress estimate:

private volatile int progressPercent;
void reportProgress(int progress) {
progressPercent = progress;
}

This works when each assignment stands on its own. If several writers must derive a new value from the current one, an atomic class or lock is required instead.

Double-checked locking#

In a correctly implemented double-checked locking singleton, volatile prevents publication of the reference from being observed separately from construction of the object:

public final class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
Singleton result = instance;
if (result == null) {
synchronized (Singleton.class) {
result = instance;
if (result == null) {
result = new Singleton();
instance = result;
}
}
}
return result;
}
}

Without volatile, another thread could observe a published reference without the visibility guarantees needed to observe the object’s fully initialized state.

When Not to Use volatile#

Compound operations#

Do not rely on volatile for read-modify-write operations such as count++, count += 2, or check-then-act logic. Another thread can intervene between the read and write, causing lost updates or decisions based on obsolete state.

Use synchronized, a Lock, or an appropriate class from java.util.concurrent.atomic, such as AtomicInteger.

Invariants involving multiple variables#

Marking several fields volatile does not make changes to them a single transaction. For example, two independent volatile fields cannot by themselves preserve an invariant such as lowerLimit < upperLimit; a reader may observe one updated value and one previous value.

Protect related fields with the same lock, or combine them into one immutable value object and publish that object through a single volatile reference.

Java Volatile Explained: Visibility, Ordering, and Memory Barriers
https://astro-nyc.pages.dev/posts/java-volatile-visibility-ordering-memory-barriers/
Author
Hari
Published at
2026-09-02
License
CC BY-NC-SA 4.0