Definition
- 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 -= amounthas 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 highwithlow <= highinvariant. 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.
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
}
}
Volatile: 167234
Atomic: 200000
Where does volatile fail for check-then-act?
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?
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:
- Only one thread writes the variable (or writes are independent of the current value).
- No compound operations (the new value does not depend on the old value).
- The variable does not participate in a multi-variable invariant.
| 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):
VarHandleprovides fine-grained memory ordering modes (plain, opaque, acquire/release, volatile) beyond the all-or-nothing of thevolatilekeyword. - Immutable object swap:
volatile ImmutableConfig configis 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
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.
---