Back to volatile
Q2Question 2 of 5

Where does volatile fail?

Key Concepts
  • volatile fails for compound actions like counter++ because read-modify-write is three operations.
  • volatile fails for check-then-act: the condition can change between the check and the action.
  • volatile fails for multi-variable invariants: updating two volatile variables is not atomic.
  • Fix compound actions with AtomicInteger; fix check-then-act with synchronized; fix multi-variable with an immutable record behind a single volatile reference.
  • volatile is sufficient only for simple read/write of a single variable with no compound operations.
  • The rule: if the new value depends on the old value, volatile is not enough.

Definition

volatile fails for compound actions (counter++), check-then-act (if-then-modify), and multi-variable invariants because it guarantees visibility of individual reads and writes but not atomicity of compound operations.
  • Compound actions – counter++ is read-modify-write (three operations). Volatile ensures each operation accesses main memory, but two threads can read the same value between the read and write.
  • Check-then-act – if (volatileVar >= threshold) volatileVar -= amount has a gap between the check and the act. Another thread can change the value in that gap.
  • Multi-variable invariants – volatile int low; volatile int high with low <= high invariant. Updating two volatile variables is two separate writes. Between them, a reader sees an inconsistent state.
  • Fix – compound actions: AtomicInteger. Check-then-act: synchronized. Multi-variable: wrap in an immutable record behind a single volatile reference.

A volatile whiteboard always shows the latest number, but: two tellers read "100" from the whiteboard, both add 1, both write "101" (expected 102). The whiteboard shows the latest value, but simultaneous reads and writes still lose updates. For multi-variable: "opening hours" on two separate whiteboards (9AM and 5PM). One updates to 10AM before the other updates from 5PM. A reader briefly sees 10AM-5PM (stale) instead of 10AM-6PM (correct).

Understanding the Concepts

Where does volatile fail for compound actions?

counter++ looks atomic but is three separate CPU instructions: load, add, store.

JAVA
package com.bank.core;

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileCompoundFail {
    private static volatile int volatileCounter = 0;
    private static final AtomicInteger atomicCounter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 100000; i++) {
                volatileCounter++;              // RACE: not atomic
                atomicCounter.incrementAndGet(); // SAFE: CAS-based
            }
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start(); t2.start();
        t1.join();  t2.join();

        System.out.println("Volatile: " + volatileCounter);  // less than 200000
        System.out.println("Atomic:   " + atomicCounter.get()); // exactly 200000
    }
}
TEXT
Volatile: 167234
Atomic:   200000

Where does volatile fail for check-then-act?

JAVA
package com.bank.core;

import java.math.BigDecimal;

public class VolatileCheckThenActFail {
    private static volatile int balance = 100000;

    // WRONG: volatile does not make check+act atomic
    public static void unsafeWithdraw(int amount) {
        if (balance >= amount) {     // volatile read: latest value
            // Another thread can change balance HERE!
            balance -= amount;        // volatile write: but based on stale check
        }
    }

    // CORRECT: synchronized makes check+act atomic
    private static final Object lock = new Object();
    public static void safeWithdraw(int amount) {
        synchronized (lock) {
            if (balance >= amount) {
                balance -= amount;
            }
        }
    }
}

Where does volatile fail for multi-variable invariants?

JAVA
package com.bank.core;

public class VolatileMultiVarFail {
    // Invariant: low <= high
    private static volatile int low = 0;
    private static volatile int high = 10;

    // WRONG: two separate volatile writes
    public static void updateRange(int newLow, int newHigh) {
        low = newLow;    // Thread-1 writes low = 8
        // Reader thread sees low=8, high=5 HERE: INVARIANT VIOLATED!
        high = newHigh;  // Thread-1 writes high = 12
    }

    // CORRECT: immutable record behind a single volatile reference
    private static volatile int[] range = {0, 10};

    public static void updateRangeSafely(int newLow, int newHigh) {
        range = new int[]{newLow, newHigh}; // single volatile write
    }
}

When is volatile sufficient?

volatile is sufficient when all of these conditions are met:

  1. Only one thread writes the variable (or writes are independent of the current value).
  2. No compound operations (the new value does not depend on the old value).
  3. The variable does not participate in a multi-variable invariant.
DIAGRAM
| Pattern | volatile sufficient? | Fix if not |
|---|---|---|
| `volatile boolean shutdown` | Yes | N/A |
| `volatile Config config` (immutable swap) | Yes | N/A |
| `volatile int counter; counter++` | No | AtomicInteger |
| `if (vol >= n) vol -= n` | No | synchronized |
| `volatile int low; volatile int high` (low <= high) | No | Immutable record + single volatile ref |

What is the simple rule?

If the new value depends on the old value, volatile is not enough. counter++ depends on the old counter. balance -= amount depends on the old balance. Both need atomicity that volatile does not provide.

What should a senior engineer know?

  • VarHandle (Java 9): VarHandle provides fine-grained memory ordering modes (plain, opaque, acquire/release, volatile) beyond the all-or-nothing of the volatile keyword.
  • Immutable object swap: volatile ImmutableConfig config is a powerful pattern. Write a new immutable config object and assign it to the volatile reference. Readers always see a complete, consistent configuration.

Visual Overview

DIAGRAM
  volatile counter++ failure:

  Thread-1                Thread-2               counter
  ─────────               ─────────              ───────
  volatile read: 0                                0
                          volatile read: 0        0
  increment: 1                                    0
  volatile write: 1                               1
                          increment: 1            1
                          volatile write: 1       1  (should be 2!)

  Both threads read 0, both write 1. One increment lost.
  volatile ensures each read/write hits main memory,
  but the gap between read and write is still exploitable.

Key Takeaway

volatile fails in three specific patterns. Compound actions like counter++ (the new value depends on the old value, and two threads can read the same old value). Check-then-act like if (balance >= amount) balance -= amount (the condition can change between the check and the act). Multi-variable invariants like low <= high with separate volatile variables (updating two variables is not atomic).

The simple rule: if the new value depends on the old value, volatile is not enough. Use AtomicInteger for single-variable atomic operations, synchronized for multi-step compound operations, and an immutable record behind a single volatile reference for multi-variable consistency.

Remember one sentence: volatile is for visibility of simple reads and writes, not for compound operations where the new value depends on the old value.

---

Share
Use ← → arrow keys to navigate