Volatile Keyword in Java (Memory Visibility & Happens-Before)

In Java concurrency, many bugs are not caused by race conditions alone—they are caused by memory visibility problems. A thread may update a variable, but another thread may continue reading an old cached value. This leads to behavior that appears random and extremely difficult to debug.

To solve such problems, Java provides the volatile keyword. It is a lightweight synchronization mechanism that guarantees visibility and ordering for shared variables, but it does not provide full mutual exclusion like synchronized.

In this article, we will deeply explore memory visibility, the happens-before relationship, and understand when volatile is sufficient—and when it is not.

What is volatile?

When a variable is declared volatile, Java guarantees that reads and writes to that variable go directly through main memory instead of allowing threads to rely only on local cached copies.
class SharedFlag {
    volatile boolean running = true;
} 
This means when one thread changes running, other threads can immediately observe the latest value.
Modern CPUs and the JVM use caches, registers, and optimizations for performance. Because of this, each thread may temporarily work with its own copy of shared data.

Without proper synchronization, one thread's update may not become visible to another thread immediately.

Without volatile

class Worker implements Runnable {
    boolean running = true;

    public void run() {
        while (running) {
            // keep working
        }
        System.out.println("Stopped");
    }
} 
Another thread does:
worker.running = false;
You may expect the loop to stop instantly, but it might continue forever because the worker thread may keep reading a cached value of true.

Using volatile

class Worker implements Runnable {
    volatile boolean running = true;

    public void run() {
        while (running) {
            // keep working
        }
        System.out.println("Stopped");
    }
}
Now when another thread writes false, the worker thread sees the update reliably.

How volatile Works

The volatile keyword provides two major guarantees:

1. Visibility Guarantee: A write to a volatile variable by one thread becomes visible to all other threads reading that variable.

2. Ordering Guarantee: Code written before updating a volatile variable will happen first, and code after reading a volatile variable will happen later. This preserves the expected execution order between threads.

This helps maintain predictable execution order between threads.

The Java Memory Model defines happens-before rules to determine when one action's result is guaranteed visible to another action.

For volatile variables: A write to a volatile variable happens-before every subsequent read of that same variable.

Example:

class Example {
    int data = 0;
    volatile boolean ready = false;

    void writer() {
        data = 42;
        ready = true;
    }

    void reader() {
        if (ready) {
            System.out.println(data);
        }
    }
}
If thread A runs writer() and thread B later sees ready == true, then thread B is guaranteed to also see data == 42.

Why? Because the volatile write to ready publishes earlier writes like data = 42. In Java, a write to a volatile variable acts like a memory flush point. This means all normal variable writes done before it (such as data = 42) must be committed and made visible before the volatile write happens.

In simple terms: ready becomes a signal that also carries earlier memory updates with it.

When volatile is Enough

Use volatile when:

1. One Thread Writes, Many Threads Read: Flags, configuration switches, shutdown signals.
 volatile boolean shutdown = false; 

2. Variable Value Does Not Depend on Current Value: Simple assignment only.
 status = true; 

3. Independent Reads/Writes: No compound operations like increment, check-then-act, or read-modify-write.

4. Safe Publication Marker: Used to signal object readiness after initialization.

When volatile is NOT Enough

The most common mistake is assuming volatile makes everything thread-safe. It does not.

1. Atomic Operations: This is unsafe even if volatile:
 volatile int count = 0; count++; 
Because count++ means:

1. Read current value
2. Add 1
3. Write new value

Multiple threads can still lose updates.

Use:
AtomicInteger count = new AtomicInteger(0); 
count.incrementAndGet(); 

2. Multiple Variables Must Stay Consistent
int x; 
int y;
If both must update together, volatile on one variable is not enough. Use synchronization.

3. Check-Then-Act Logic
 if (!initialized) { initialize(); } 
Two threads may both enter simultaneously unless synchronized properly.

4. Complex Critical Sections: If business logic must be exclusive, use locks or synchronized blocks.
volatile vs synchronized
volatile:
- Visibility guarantee
- Ordering guarantee
- No locking
- No mutual exclusion
- Lightweight
synchronized:
- Visibility guarantee
- Mutual exclusion
- Atomic critical section
- Higher overhead than volatile
Declaring a collection reference volatile does not make the collection thread-safe.
 volatile List<String> list; 
Only the reference visibility is guaranteed, not safe concurrent modifications of the list contents.

Conclusion

The volatile keyword is a powerful tool for solving memory visibility problems and establishing happens-before relationships between threads. It ensures that updates become visible promptly and prevents dangerous reordering issues.

However, volatile is not a replacement for synchronization. It cannot guarantee atomicity or protect complex shared state. Use it for flags, state signals, and simple publication patterns—but use synchronized, locks, or atomic classes when coordination becomes more complex.

In the next article, we will dive deeper into the Java Memory Model and understand how threads interact with main memory, caches, and instruction reordering.
Nagesh Chauhan
Nagesh Chauhan
Principal Engineer | Java · Spring Boot · Python · Microservices · AI/ML

Principal Engineer with 14+ years of experience in designing scalable systems using Java, Spring Boot, and Python. Specialized in microservices architecture, system design, and machine learning.

Share this Article

💬 Comments

Join the discussion