Skip to main content

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:​

  1. Memory Visibility: Every write to a volatile variable is immediately committed to main memory; every subsequent read is fetched directly from main memory.
  2. Instruction Ordering: Inserts JVM Memory Barriers (LoadLoad, StoreStore, LoadStore, StoreLoad) preventing compiler and CPU reordering across the volatile access.

What volatile Does NOT Guarantee:​

  • volatile does 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?​

  1. 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++ → 🔓
  1. 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), LongAdder shards counts across internal cell arrays to eliminate CAS spin contention.

volatile vs Atomic​

This is a very common interview question.

VolatileAtomic
Mainly provides visibilityProvides atomic operations
Does not make count++ safeMakes operations like increment safe
No compound-operation protectionSupports atomic compound operations
Good for flags/stateGood for counters and simple shared values