What is the volatile keyword in Java and when should you use it?

Java volatile keyword
Quick Answer: volatile guarantees that reads and writes to a variable go directly to main memory, ensuring all threads see the latest value — but it does not provide atomicity or mutual exclusion.

1. Short Answer

The volatile keyword in Java is a field modifier that prevents threads from caching a variable in CPU registers or thread-local caches. Every read of a volatile variable reads from main memory, and every write flushes to main memory immediately. This solves the memory visibility problem in multi-threaded programs.

  • Guarantees: Memory visibility — all threads see the most recent write.
  • Guarantees: Happens-before relationship — a write to a volatile variable happens-before every subsequent read of that variable.
  • Does NOT guarantee: Atomicity for compound operations (e.g., count++).
  • Does NOT guarantee: Mutual exclusion — multiple threads can still execute code concurrently.

2. The Memory Visibility Problem

Modern CPUs have multiple levels of cache. When a thread writes to a variable, the new value may sit in that thread's CPU cache and never be written to main memory — or the write may be delayed. Another thread reading the same variable may see a stale value from its own cache.

This is not a bug in Java — it is the result of legitimate JVM and CPU optimizations. Without volatile or synchronized, the Java Memory Model (JMM) does not guarantee when (or whether) changes made by one thread become visible to another.

Java Memory Model

The Java Memory Model (JMM), defined in the Java Language Specification, specifies exactly when one thread's actions are guaranteed to be visible to another. Without explicit synchronization, there is no such guarantee.

3. How volatile Works

When a field is declared volatile, the JVM inserts memory barriers around reads and writes to that field:

  • A write to a volatile variable flushes the new value (and any other pending writes) to main memory.
  • A read of a volatile variable invalidates the thread's local cache for that variable, forcing it to re-read from main memory.

This gives you a happens-before guarantee: if thread A writes to a volatile variable and thread B subsequently reads that variable, then everything thread A did before the write is visible to thread B after the read.

Happens-Before Rule: A write to a volatile field happens-before every subsequent read of that same field. This is the fundamental visibility contract.

4. volatile Does NOT Guarantee Atomicity

A common interview trap is confusing visibility with atomicity. Consider this:

// WRONG: volatile does not make this safe
private volatile int counter = 0;

public void increment() {
    counter++; // This is NOT atomic!
    // Equivalent to: int tmp = counter; tmp = tmp + 1; counter = tmp;
    // Two threads can interleave these three steps.
}

Even though counter is volatile, the ++ operation is a read-modify-write that involves three separate steps. Two threads can both read the same old value, both increment it, and both write back — losing one increment. For atomic compound operations, use AtomicInteger or synchronized.

5. volatile vs synchronized

Feature volatile synchronized
Memory visibilityYesYes
AtomicityNo (simple reads/writes only)Yes (any code block)
Mutual exclusionNoYes
BlockingNo — never blocks threadsYes — other threads wait for the lock
PerformanceFaster (no locking overhead)Slower (lock acquisition/release)
Best forSimple flags, status fieldsCompound operations, critical sections

6. Code Example

Here is a correct use of volatile for a stop-flag pattern — one of the most common real-world uses:

public class WorkerThread extends Thread {

    // volatile ensures the main thread's write is visible to this thread
    private volatile boolean running = true;

    @Override
    public void run() {
        System.out.println("Worker started");
        while (running) {
            // Do some work
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        System.out.println("Worker stopped");
    }

    public void shutdown() {
        running = false; // Write is immediately visible to the worker thread
    }

    public static void main(String[] args) throws InterruptedException {
        WorkerThread worker = new WorkerThread();
        worker.start();

        Thread.sleep(500);
        worker.shutdown(); // Signals the worker to stop
        worker.join();
        System.out.println("Done");
    }
}
// Output:
// Worker started
// Worker stopped
// Done
Without volatile

Without volatile, the JIT compiler might hoist the running check out of the loop entirely (seeing it never changes within the loop body), causing the worker thread to loop forever even after shutdown() is called.

7. Double-Checked Locking Pattern

The classic use of volatile in production code is the double-checked locking singleton pattern, introduced in Java 5:

public class Singleton {

    // volatile is REQUIRED here — without it, the pattern is broken
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                // First check (no lock)
            synchronized (Singleton.class) {
                if (instance == null) {        // Second check (with lock)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
// Without volatile, the JVM could reorder the steps inside new Singleton():
// 1. Allocate memory
// 2. Assign reference to 'instance'  <-- reordered BEFORE step 3
// 3. Call constructor
// Another thread could see a non-null but partially constructed object.

The volatile on instance prevents the JVM from reordering the constructor call and the assignment, ensuring no thread ever observes a partially constructed singleton.

8. When to Use volatile

Use volatile when all of these conditions are true:

  • The variable is written by one thread and read by one or more other threads (or written independently by multiple threads with no dependency on the previous value).
  • The operation on the variable is a simple read or write — not a compound operation like increment or compare-and-swap.
  • You do not need mutual exclusion — threads do not need to be blocked.

Common valid uses:

  • Boolean stop/running flag for a worker thread.
  • A status or state field updated by one thread and polled by others.
  • The instance field in a double-checked locking singleton.

Do NOT use volatile when:

  • Multiple threads perform compound operations (use AtomicInteger, AtomicReference, or synchronized).
  • You need to protect a block of related operations (use synchronized or ReentrantLock).

9. Why This Question Matters in Interviews

Interviewers ask about volatile to test depth of knowledge in Java concurrency. A surface-level answer ("it prevents caching") is expected. Strong candidates go further:

  • Mention the Java Memory Model and happens-before semantics.
  • Clearly state that volatile does not provide atomicity for compound operations.
  • Contrast it with synchronized (mutual exclusion vs. visibility only).
  • Bring up the double-checked locking pattern as a real use case.
  • Mention AtomicInteger / java.util.concurrent.atomic as the right tool for atomic operations.

10. Conclusion

The volatile keyword is a lightweight synchronization mechanism that solves one specific problem: ensuring memory visibility across threads without the overhead of locking. It guarantees that a write to a volatile field happens-before any subsequent read of that field by any thread. However, it does not provide atomicity or mutual exclusion. Use it for simple flags and status fields; reach for synchronized, ReentrantLock, or the java.util.concurrent.atomic package when you need compound operations or critical sections.

Subscribe to Our Newsletter

Get the latest updates and exclusive content delivered to your inbox!