Skip to content

Volatile

Here is the updated interview preparation guide strictly focused on Java. It covers how volatile interacts with the Java Memory Model (JMM), thread visibility, instruction reordering, and atomic vs non-atomic operations from basic to advanced levels.


Basic Level

Q1: What is the volatile keyword in Java?

Answer: In Java, volatile is a field modifier used to mark a variable as "stored in main memory."

It tells the Java Virtual Machine (JVM) and the CPU never to cache the variable's value in thread-local CPU registers or L1/L2 caches. Every read of a volatile variable is fetched directly from main RAM, and every write is flushed immediately back to main RAM.


Q2: What core concurrency problem does volatile solve?

Answer: It solves the Visibility Problem.

By default, in multi-core processors, each thread running on a separate core keeps a local copy of variables in its CPU cache for performance. If Thread A updates a shared variable, Thread B running on another core might not see that update immediately (or ever) because it reads from its own cache.

Marking a variable volatile guarantees cross-thread visibility: any thread that reads the variable will always see the most recent value written by any other thread.

// Common Use-Case: Shutdown Flag
public class ServerTask implements Runnable {
    // Volatile guarantees loop termination when flag changes in another thread
    private volatile boolean keepRunning = true;

    public void stopServer() {
        this.keepRunning = false; // Immediately visible to worker thread
    }

    @Override
    public void run() {
        while (keepRunning) {
            // Processing incoming requests...
        }
    }
}

Intermediate Level

Q3: Does volatile make operations thread-safe or atomic?

Answer: No. volatile guarantees Visibility and Ordering, but NOT Atomicity.

A classic interview trick question asks if volatile int count = 0; makes count++ thread-safe. It does not.

The operation count++ is not a single atomic instruction. It involves three distinct steps:

  1. Read count from main memory.
  2. Increment the value in the CPU register.
  3. Write the updated value back to main memory.

If two threads execute count++ concurrently, both can read the same starting value before either completes the write, resulting in a lost update (a Race Condition).


Q4: When should you use volatile vs synchronized?

Answer:

Feature volatile synchronized
Mechanics Non-blocking variable modifier. Blocking mutual exclusion lock.
Guarantees Visibility & Ordering only. Visibility, Ordering, and Atomicity.
Performance High (no thread context switching or lock contention). Lower (threads block/wait for lock acquisition).
Best Used For Status flags, state indicators, single reference updates. Compound operations (count++), invariant checks, multi-field updates.

Q5: When should you AVOID using volatile in Java?

Answer: You should avoid volatile in the following scenarios:

  1. Compound operations / Invariant updates: Any time a write depends on the current value (e.g., count++, total += value, or checking if (x == 5) x = 10;). Use AtomicInteger or synchronized instead.
  2. Dependent variables: When multiple variables must be updated together atomically (e.g., updating both x and y coordinates).
  3. Immutability: If a field is declared final, it is already thread-safe after construction and does not need volatile.
  4. Local variables: volatile cannot be applied to method-local variables; local variables reside on the thread stack and are never shared across threads.

Advanced Level

Q6: What is the "Happens-Before" relationship in JMM, and how does volatile affect it?

Answer: The Java Memory Model (JMM) defines execution constraints via the Happens-Before principle. If operation A happens-before operation B, the memory writes done by A are guaranteed to be visible to B.

For volatile variables:

  1. Volatile Write Rule: A write to a volatile variable happens-before every subsequent read of that same volatile variable.
  2. Piggybacking (Transitivity) Rule: When Thread A writes to a volatile variable, all non-volatile variables written by Thread A before that point are also flushed to main memory and made visible to Thread B when it reads that same volatile variable.
public class VisibilityExample {
    private int data = 0;             // Non-volatile
    private volatile boolean ready = false; // Volatile flag

    public void writerThread() {
        data = 42;                    // Step 1
        ready = true;                 // Step 2 (Volatile write flushes 'data' too!)
    }

    public void readerThread() {
        if (ready) {                  // Step 3 (Volatile read)
            System.out.println(data); // Step 4 (Guaranteed to print 42, not 0)
        }
    }
}

Q7: How does volatile prevent Instruction Reordering under the hood?

Answer: Modern JIT compilers and CPU architectures reorder bytecode/machine instructions to maximize CPU instruction pipeline usage. However, reordering across thread boundaries can break logic.

To prevent illegal reordering, the JVM inserts Memory Barriers (Fences) into generated assembly code around volatile accesses:

  • StoreStore Barrier: Inserted before a volatile write. Ensures all preceding normal writes are completed before the volatile write occurs.
  • StoreLoad Barrier: Inserted after a volatile write. Ensures the volatile write is visible to all processors before any subsequent read/write.
  • LoadLoad / LoadStore Barrier: Inserted after a volatile read. Ensures subsequent reads/writes cannot be reordered before this volatile read.

Q8: Why is volatile strictly required in the Double-Checked Locking Singleton pattern?

Answer: Without volatile, Double-Checked Locking suffers from object creation reordering.

public class Singleton {
    // MUST be volatile
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                  // First Check
            synchronized (Singleton.class) {
                if (instance == null) {          // Second Check
                    instance = new Singleton();  // Problematic without volatile!
                }
            }
        }
        return instance;
    }
}

Why it fails without volatile: Creating an object (instance = new Singleton()) executes in 3 steps:

  1. Allocate memory.
  2. Run constructor (initialize instance fields).
  3. Assign pointer instance to the allocated memory location.

Without volatile, the compiler/CPU can reorder execution to 1 $\rightarrow$ 3 $\rightarrow$ 2.

If Thread A executes step 1 and step 3, instance is no longer null, but the constructor (step 2) hasn't finished. If Thread B calls getInstance(), it passes the first check, skips lock acquisition, and returns a partially constructed object, leading to unpredictable crashes or corrupted data in Thread B.

volatile forces step 2 to complete before step 3 becomes visible to other threads.


Quick Interview Cheat Sheet

  • Primary Purpose: Thread visibility & instruction ordering prevention.
  • Memory Mechanism: Bypasses thread-local CPU caches; uses Memory Barriers.
  • Guarantees: Visibility + Ordering.
  • Does NOT Guarantee: Atomicity (not safe for count++).
  • Key Alternatives: java.util.concurrent.atomic classes (e.g., AtomicInteger), ReentrantLock, synchronized.