Back to volatile
Q1Question 1 of 5

What does the volatile keyword do?

Key Concepts
  • volatile guarantees visibility: every read comes from main memory, every write goes to main memory.
  • Without volatile, the JIT compiler may cache a variable in a CPU register, and other threads never see updates.
  • volatile does NOT guarantee atomicity: counter++ on a volatile int is still a race condition.
  • volatile establishes a happens-before relationship: everything before a volatile write is visible after the next volatile read.
  • Use volatile for flags (shutdown boolean), status variables, and single-writer-many-readers patterns.
  • For read-modify-write operations like counters, use AtomicInteger or synchronized instead of volatile.

Definition

volatile guarantees that every write is immediately visible to all threads (visibility), but it does not make compound operations like counter++ atomic.
  • Visibility – a volatile write flushes the value to main memory and invalidates other CPUs' caches. A volatile read always fetches from main memory, never from a CPU register or cache line.
  • Happens-before – a write to a volatile variable happens-before every subsequent read of that variable by any thread. All memory writes performed before the volatile write become visible to the thread that performs the subsequent volatile read.
  • No atomicity – volatile int counter; counter++ is still three operations (read, increment, write). Volatile ensures each operation accesses main memory, but two threads can still read the same value between the read and write steps.
  • Memory barrier – on x86, a volatile write inserts a StoreLoad barrier (MFENCE or LOCK prefix). This prevents the CPU from reordering instructions across the barrier.

Without volatile, each teller has a personal notepad with Rajesh's balance. Teller-1 updates the balance on their notepad. Teller-2's notepad still shows the old value because it was never refreshed. With volatile, there is one shared whiteboard (main memory). Every write goes to the whiteboard. Every read comes from the whiteboard. All tellers see the latest value. But if two tellers read the whiteboard simultaneously, both increment, and both write, one increment is lost. The whiteboard ensures visibility, not atomicity.

Understanding the Concepts

What does volatile guarantee?

volatile provides two guarantees: visibility and ordering. It does not provide atomicity.

How does visibility work without volatile?

Without volatile, the JIT compiler may optimize a variable read into a CPU register. The thread reads the register repeatedly and never checks main memory.

JAVA
package com.bank.core;

public class VisibilityProblem {
    private static boolean running = true; // NOT volatile

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            // JIT may hoist: if (running) while(true) { ... }
            while (running) {
                // busy work
            }
            System.out.println("Worker stopped");
        });
        worker.start();
        Thread.sleep(1000);
        running = false; // written to main memory, but worker may never see it
        System.out.println("Main set running=false");
        worker.join(2000);
        if (worker.isAlive()) {
            System.out.println("Worker is STILL running! Visibility bug.");
        }
    }
}

How does volatile fix the visibility problem?

JAVA
package com.bank.core;

public class VisibilityFix {
    private static volatile boolean running = true; // volatile!

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (running) { // volatile read: checks main memory each time
                // busy work
            }
            System.out.println("Worker stopped");
        });
        worker.start();
        Thread.sleep(1000);
        running = false; // volatile write: flushes to main memory
        worker.join();
    }
}
TEXT
Worker stopped

Why does volatile NOT make counter++ atomic?

counter++ is three separate operations: read the value, increment it, and write it back. Volatile ensures each individual read or write accesses main memory, but two threads can still interleave between steps.

JAVA
package com.bank.core;

public class VolatileCounterRace {
    private static volatile int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) counter++; // NOT atomic!
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) counter++;
        });
        t1.start(); t2.start();
        t1.join();  t2.join();
        System.out.println("Counter: " + counter);
        // Expected: 200000. Actual: less (e.g., 156789)
    }
}
TEXT
Counter: 156789

What is the happens-before relationship?

The happens-before relationship guarantees that all writes before a volatile write are visible to any thread that subsequently reads the volatile variable.

JAVA
package com.bank.core;

public class HappensBeforeDemo {
    private static int data = 0;           // non-volatile
    private static volatile boolean ready = false; // volatile

    public static void main(String[] args) throws InterruptedException {
        Thread writer = new Thread(() -> {
            data = 42;         // write to non-volatile
            ready = true;      // volatile write: establishes happens-before
        });

        Thread reader = new Thread(() -> {
            while (!ready) { } // volatile read: sees everything before the write
            System.out.println("Data: " + data); // guaranteed to see 42
        });

        writer.start();
        reader.start();
        writer.join();
        reader.join();
    }
}
TEXT
Data: 42

When should you use volatile?

| Use case | Volatile? | Why |
|---|---|---|
| Shutdown flag: volatile boolean running | Yes | Single writer, many readers, no compound op |
| Latest price: volatile BigDecimal price | Yes | One writer publishes, many readers consume |
| Double-checked locking: volatile Singleton | Yes | Prevents seeing partially constructed object |
| Counter: counter++ | No | Compound operation, use AtomicInteger |
| Balance check+debit | No | Check-then-act, use synchronized |

What should a senior engineer know?

  • Memory barriers: on x86, a volatile write inserts a StoreLoad barrier. On ARM, both LoadLoad and StoreStore barriers are needed. The JVM handles this automatically.
  • volatile vs synchronized: volatile is lighter (no monitor acquisition) but weaker (no atomicity, no mutual exclusion). synchronized provides all three guarantees.
  • volatile long/double: on 32-bit JVMs, reading a non-volatile long or double is not atomic (it is two 32-bit reads). Declaring them volatile guarantees atomic read/write. On 64-bit JVMs, this is already atomic in practice.

Visual Overview

DIAGRAM
  Without volatile:
  ┌────────────┐              ┌────────────┐
  │ Thread-1    │              │ Thread-2    │
  │ CPU Cache   │              │ CPU Cache   │
  │ running=true│              │ running=true│ (stale!)
  └──────┬─────┘              └──────┬─────┘
         │                           │
         ▼                           ▼
  ┌─────────────────────────────────────────┐
  │           Main Memory                    │
  │           running = false                │
  └─────────────────────────────────────────┘
  Thread-2 never reads main memory: infinite loop!

  With volatile:
  ┌────────────┐   volatile   ┌────────────┐
  │ Thread-1    │   write      │ Thread-2    │
  │ writes false│──────────▶  │ reads false │
  └────────────┘   main mem   └────────────┘
  Every volatile write flushes to main memory.
  Every volatile read fetches from main memory.

Key Takeaway

volatile guarantees visibility: every write goes to main memory and every read comes from main memory. Without it, the JIT compiler may cache a variable in a CPU register, and a reading thread never sees updates from another thread. The classic use case is a volatile boolean running shutdown flag.

However, volatile does NOT guarantee atomicity. volatile int counter; counter++ is still a race condition because it is three operations (read, increment, write) and another thread can interleave. For counters, use AtomicInteger. For compound check-then-act operations, use synchronized.

Remember one sentence: volatile ensures every thread sees the latest value (visibility), but it does not make compound operations like counter++ atomic.

---

Share
Use ← → arrow keys to navigate