6.3 Volatile Memory Barriers & Hardware-Level Atomic Variables
← Back to Chapter 6: Multithreading & Concurrency
Multi-core modern CPUs execute instructions with local L1/L2 hardware caches and out-of-order execution pipelines.
1. The volatile Keyword: Visibility & Reordering
Volatile is mainly used to solve the visibility problem between threads.
Suppose we have:
boolean running = true;
Two threads are working:
Thread 1 Thread 2
│ │
│ running = false │
│ │
│ while(running)
│ │
Thread 1 changes:
running = false
But Thread 2 might have an old copy of running in its CPU cache and may not immediately see the updated value.
That's the visibility problem.
public class WorkerTask implements Runnable {
// volatile ensures writes in one CPU core are instantly flushed to main memory
private volatile boolean running = true;
public void stop() {
this.running = false;
}
@Override
public void run() {
while (running) {
// Without volatile, this loop might never terminate due to CPU register caching!
}
}
}
"Whenever a thread reads this variable, make sure it sees the latest visible value rather than relying on a stale cached copy."
What volatile Guarantees:
- Memory Visibility: Every write to a volatile variable is immediately committed to main memory; every subsequent read is fetched directly from main memory.
- Instruction Ordering: Inserts JVM Memory Barriers (LoadLoad, StoreStore, LoadStore, StoreLoad) preventing compiler and CPU reordering across the volatile access.
What volatile Does NOT Guarantee:
volatiledoes not guarantee atomicity for compound operations!count++consists of 3 distinct bytecode operations:getfield,iadd,putfield. Two threads can read the same value concurrently, causing lost updates.
2. Atomicity (java.util.concurrent.atomic)
An operation happens completely, without another thread interfering in the middle. This is called Atomicity.
Java provides lock-free thread-safe primitives backed by low-level CPU hardware instructions (CAS: Compare-And-Swap):
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
// Under the hood: CPU LOCK CMPXCHG instruction
count.incrementAndGet();
}
public int get() {
return count.get();
}
}
How do we solve the atomicity problem?
- Option 1: synchronized
synchronized void increment() {
count++;
}
Now only one thread can execute the protected code at a time.
Thread 1 → 🔒 → count++ → 🔓
Thread 2 → waits
Thread 2 → 🔒 → count++ → 🔓
- Option 2: Atomic classes Java provides classes such as:
AtomicInteger
AtomicLong
AtomicBoolean
Example:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
incrementAndGet() performs the increment atomically.
How Compare-And-Swap (CAS) Works
CPU Memory Address: V
▲
│
┌──────────────────┴──────────────────┐
│ │
Thread A reads: Thread B reads:
Expected: 10 Expected: 10
Update to: 11 Update to: 12
│ │
CAS(V, 10, 11) CAS(V, 10, 12)
│ │
SUCCEEDS! V = 11 FAILS! (V is now 11)
│
Retries loop with
new Expected = 11
- Zero OS Context Switching: Threads do not enter OS kernel sleep; they spin briefly in user space until the CAS succeeds, yielding massively superior throughput under high concurrency.
LongAdder: For ultra-high write contention (metrics, counters),LongAddershards counts across internal cell arrays to eliminate CAS spin contention.
volatile vs Atomic
This is a very common interview question.
| Volatile | Atomic |
|---|---|
| Mainly provides visibility | Provides atomic operations |
Does not make count++ safe | Makes operations like increment safe |
| No compound-operation protection | Supports atomic compound operations |
| Good for flags/state | Good for counters and simple shared values |