Deep Dive into Java Concurrency (Part 3): The Low‑Level Mechanics of volatile and CAS
This article explains Java's volatile and CAS primitives, detailing volatile's visibility and ordering guarantees, the memory barriers the JVM inserts, CAS's atomic compare‑and‑swap operation, common pitfalls like the ABA problem, and how their combination enables lock‑free thread safety, with code examples and interview tips.
The article continues a series on Java concurrency, focusing on the lightweight primitives volatile and CAS, which form the foundation of the JUC package.
1. volatile memory semantics
It first asks what volatile guarantees and what it does not.
Visibility : writes to a volatile variable are immediately flushed to main memory; reads fetch the latest value.
Ordering : prevents instruction reordering via memory barriers.
Atomicity : volatile count++ is not atomic.
1.1 volatile happens‑before rule
Write to a volatile variable happens‑before any subsequent read of that variable.
Thread A Thread B
─────────────────────────────────────────────────
volatile boolean flag = true; if (flag) { ... }
│ ▲
│ happens‑before │
└─────────────────────────┘
All actions before Thread A writes flag become visible to Thread B.This establishes a lightweight synchronization boundary.
1.2 volatile memory barriers
To implement the semantics, the JVM inserts memory barriers around volatile reads and writes:
StoreStore barrier before a volatile write – prevents earlier ordinary writes from being reordered after the volatile write.
StoreLoad barrier after a volatile write – the most expensive barrier; flushes the store buffer to main memory.
LoadLoad + LoadStore barriers after a volatile read – ensures later reads/writes are not reordered before the volatile read.
The StoreLoad barrier is the costliest, which explains why volatile writes are slower than ordinary writes.
2. CAS (Compare And Swap) principle
CAS is the cornerstone of the entire JUC package; all high‑level atomic classes ultimately rely on it.
2.1 What is CAS?
CAS is an atomic operation with three parameters:
V : the variable to update.
E : the expected (old) value.
N : the new value.
Semantics: the variable is updated to N only if its current value equals E; otherwise nothing happens.
CAS is implemented by a single CPU instruction (e.g., cmpxchg) and is therefore atomic and non‑interruptible.
┌─────────────────────────────────────────────────────────────────┐
│ CAS execution flow │
├─────────────────────────────────────────────────────────────────┤
│ if (V == E) { // memory value equals expected value │
│ V = N; // update to new value │
│ return true; // success │
│ } else { │
│ return false; // failure (value changed by another) │
│ } │
│ • CPU guarantees atomicity │
│ • No lock, no thread blocking │
│ • Caller decides whether to retry on failure │
└─────────────────────────────────────────────────────────────────┘2.2 Java’s CAS implementation
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.compareAndSwapInt signature
public final native boolean compareAndSwapInt(Object o, long offset,
int expected, int x);2.3 Three classic CAS problems
Problem 1: ABA
Scenario: a variable changes A → B → A; CAS cannot detect the intermediate change.
// Thread 1 wants to change A to C
// Thread 2 changes A to B, then B back to A
// Thread 1 sees V == E (A == A) and CAS succeeds, but the value is not the original ASolution: use a version number or timestamp, e.g., AtomicStampedReference.
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int stamp = ref.getStamp();
ref.compareAndSet("A", "B", stamp, stamp + 1);Problem 2: Long spin loops
Spinning CAS that fails repeatedly consumes CPU.
while (!atomic.compareAndSet(expected, newValue)) {
// spin until success
}Solution: limit spin count or apply back‑off strategies.
Problem 3: Single‑variable limitation
CAS can only atomically modify one variable.
Wrap multiple values in an object and use AtomicReference.
Fall back to a lock when multiple variables must be updated together.
3. Unsafe class
All Java CAS operations eventually delegate to the Unsafe class, which provides low‑level memory access.
public final class Unsafe {
public static Unsafe getUnsafe() { ... }
public native boolean compareAndSwapInt(Object o, long offset,
int expected, int x);
public native boolean compareAndSwapLong(Object o, long offset,
long expected, long x);
public native boolean compareAndSwapObject(Object o, long offset,
Object expected, Object x);
public native long allocateMemory(long bytes);
public native void freeMemory(long address);
public native long objectFieldOffset(Field field);
}4. volatile + CAS = lock‑free concurrency
volatileguarantees visibility and ordering; CAS guarantees atomicity. Their combination yields lock‑free thread safety.
// Lock‑free counter
public class Counter {
private volatile int value;
public void increment() {
int old, newVal;
do {
old = value;
newVal = old + 1;
} while (!compareAndSwapInt(valueOffset, old, newVal));
// spin until success
}
}5. Interview‑ready answer
When asked “What is the relationship between volatile and CAS?” a concise answer can be: volatile provides visibility and ordering but not atomicity.
CAS is a CPU atomic instruction that provides atomicity.
The combination volatile + CAS achieves lock‑free thread safety.
The entire JUC package (Atomic*, AQS) is built on this combination.
6. Next preview
The next article will deep‑dive into AQS source code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Coder Trainee
Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
