Back to Blog
Java

Java volatile Keyword: Visibility Without Atomicity

java volatile keyword: Explains what the volatile keyword guarantees in Java, where it applies, and why it cannot replace synchronized or atomic classes.

java concurrencyjava memory modelvolatilethread safetyjava synchronization
Diagram showing a volatile field being written by one thread and read by multiple threads through a shared memory barrier.

The volatile keyword in Java controls how a field is accessed across threads. Understanding the java volatile keyword starts with one fact: it guarantees visibility, not atomicity. A volatile field ensures that all threads see the most recently written value, but it does not make compound operations like count++ safe.

What the volatile Keyword Guarantees

When a field is declared volatile, the Java Memory Model requires that a write to that field happens-before any subsequent read of that same field. In practical terms, the JVM ensures the value is read from main memory rather than from a thread-local cache, and writes are published to main memory immediately.

This guarantee has two parts. First, the write is made visible to all threads that read the field afterward. Second, the happens-before relationship extends transitively: everything the writing thread did before the volatile write is also visible to any thread that reads the volatile field.

What volatile does not provide is mutual exclusion. Two threads can still interleave their operations on a volatile field. The keyword only orders reads and writes relative to each other; it does not prevent a thread from observing a stale intermediate state during a compound operation.

How volatile Fits Into the Java Memory Model

Each thread in the JVM may keep copies of variables in its own working memory. Without synchronization, one thread may not see another thread's update because the update remains in the writing thread's local cache. The volatile keyword establishes a happens-before edge: a write to a volatile field happens-before every subsequent read of that same field.

This is the same ordering guarantee that synchronized blocks provide, but without locking. The JVM inserts memory barriers at the appropriate points to enforce this ordering, which is why volatile reads and writes are cheaper than acquiring a monitor lock.

The happens-before relationship is transitive. If thread A writes to a volatile field after updating several regular fields, and thread B reads that volatile field, then B sees all of A's earlier writes. This transitivity is what makes the publish-once pattern work.

A Minimal Working Example

The classic scenario where volatile matters is a flag that controls whether a worker thread keeps running.

public class Worker { private volatile boolean running = true; public void stop() { running = false; } public void run() { while (running) { // perform work } } }

Without volatile, the JIT compiler is allowed to hoist the running read out of the loop, effectively turning the loop into an infinite one even after stop() is called. The volatile declaration prevents that optimization because the compiler must assume the value can change at any time.

This pattern is safe because the flag is written by one thread and read by others, and the write does not depend on the current value. There is no read-modify-write cycle on the flag itself.

Where volatile Is the Right Choice

Volatile is appropriate when a field satisfies all of these conditions:

  • The field is written by only one thread, or writes are externally synchronized.
  • The field is read by multiple threads.
  • The value written does not depend on the current value of the field.

Common examples include status flags that signal shutdown or cancellation, configuration values that are updated infrequently and read frequently, and publish-once references where the object is safely constructed before publication.

public class Cache { private volatile Map<String, String> entries = Collections.emptyMap(); public void update(Map<String, String> newEntries) { entries = new HashMap<>(newEntries); } public String get(String key) { return entries.get(key); } }

Here the volatile reference ensures that readers see either the old map or the new map, never a partially constructed one. The map itself is immutable after publication, so no further synchronization is needed.

Where volatile Fails: Compound Actions

Volatile does not help with operations that read a value, modify it, and write it back. A counter is the canonical example:

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

The expression count++ is three separate operations: read the current value, add one, write the result back. Two threads can both read the same value, both increment it, and both write the same result, losing one increment. Volatile visibility does nothing to prevent this interleaving.

For compound actions, you need either synchronized, an AtomicInteger, or a lock. The choice depends on the operation being performed and the contention level.

volatile vs synchronized vs Atomic Classes

MechanismGuaranteesCostBest for
volatileVisibility onlyLowestFlags, published references
synchronizedVisibility + mutual exclusionHigherCompound actions, invariants
Atomic classesVisibility + atomic operationsModerateCounters, compare-and-set

Synchronized provides both mutual exclusion and visibility. Atomic classes use hardware compare-and-set instructions to provide atomic operations without blocking. Volatile provides only the visibility half of the equation.

A practical rule: if you need to increment, decrement, or compare-and-swap, use an Atomic class. If you need to protect an invariant involving multiple fields, use synchronized. If you only need to publish a value that is written by one thread and read by many, volatile is sufficient.

Performance Characteristics of volatile

Volatile reads and writes are cheaper than synchronized blocks because they do not acquire a monitor lock. However, they are not free. On most hardware, a volatile write requires a memory barrier that flushes the write buffer, which can stall the pipeline. Volatile reads are typically cheaper than writes on x86, but the exact cost varies by architecture.

The performance advantage of volatile comes from avoiding lock contention. If multiple threads contend on a synchronized block, threads may block and be descheduled. Volatile never blocks a thread. That makes it attractive for high-frequency reads of a rarely updated value.

Common Misconceptions and Edge Cases

A frequent mistake is assuming volatile makes a field thread-safe in general. It does not. It only guarantees that reads see the latest write. Any invariant that involves more than one field, or a read-modify-write cycle, still requires stronger synchronization.

Another edge case: volatile does not make an object's internal state visible. If a volatile reference points to a mutable object, readers can still observe inconsistent internal state unless the object itself is thread-safe or immutable.

A subtle point is that volatile does not help with ordering across multiple volatile fields. Each volatile field has its own happens-before edges, but there is no total order between writes to different volatile fields unless the reads and writes are structured to create one. If you need to coordinate multiple fields atomically, synchronized is the safer choice.

java volatile keyword: Practical Usage and Code Examples | RYUSLOG DEV