Skip to main content
The volatile keyword in Java is a field modifier that dictates how the Java Virtual Machine (JVM) and the underlying hardware handle memory visibility and instruction ordering for a specific variable. It guarantees that any thread reading a volatile field will always see the most recently written value, effectively disabling thread-local caching for that variable.
public class SharedState {
    private volatile boolean active;
    private volatile long counter;
}

The Java Memory Model (JMM) Semantics

The behavior of volatile is strictly defined by the Java Memory Model, which provides two primary guarantees:

1. Visibility Guarantee

In a multi-threaded environment, threads typically cache variables in CPU registers or local cache tiers (L1/L2/L3) to optimize performance. This can lead to cache coherence issues where one thread updates a variable, but other threads continue to read stale data from their local caches. Declaring a field as volatile instructs the JVM to bypass these local caches. Every write to a volatile field is immediately flushed to main memory, and every read of a volatile field is fetched directly from main memory.

2. Ordering Guarantee (Happens-Before Relationship)

Compilers, the JVM, and CPUs frequently reorder instructions to optimize execution pipelines. The JMM restricts this reordering around volatile fields by establishing a strict happens-before relationship:
  • A write to a volatile field happens-before every subsequent read of that same field.
  • Memory Barriers: To enforce this, the JVM inserts hardware-level memory barriers (fences).
    • A StoreStore and LoadStore barrier prevents operations before the volatile write from being reordered after it.
    • A StoreLoad barrier ensures the volatile write is visible before any subsequent reads.
    • A LoadLoad and LoadStore barrier prevents operations after the volatile read from being reordered before it.
Consequently, when a thread writes to a volatile variable, all variables (even non-volatile ones) updated by that thread prior to the write become visible to any other thread that subsequently reads the volatile variable.

Atomicity Constraints

A critical distinction in Java is that volatile ensures visibility and ordering, but it does not guarantee atomicity for compound operations.
  • Primitive Reads/Writes: Reads and writes to a volatile field are atomic. This includes 64-bit primitives (long and double). Without volatile, the JMM permits 64-bit reads and writes to be treated as non-atomic 64-bit operations (JLS 17.7). The JVM may execute them as two separate 32-bit operations, which can result in “half-writes” (reading a partially updated value where 32 bits belong to one write and 32 bits belong to another). Declaring the field as volatile enforces atomic reads and writes of the entire 64-bit value, preventing half-writes.
  • Compound Operations: Operations that require a read-modify-write sequence are not atomic, even if the field is volatile.
public class Counter {
    private volatile int count = 0;

    public void increment() {
        // NOT ATOMIC: This is a read, an addition, and a write.
        // volatile does not prevent race conditions here.
        count++; 
    }
}
Because volatile does not acquire monitors or locks, it cannot provide mutual exclusion. If multiple threads are concurrently writing to a field based on its current state, volatile is insufficient, and synchronization mechanisms (like synchronized blocks or java.util.concurrent.atomic classes) must be used instead.
Tired of Poor Java Skills? Fix That With Deep Grasping!Learn More