🧵 Section 9 · Question #8

What are Atomic classes

Unlike locks (synchronized, ReentrantLock), which suspend contending threads into BLOCKED or WAITING states via kernel calls, atomic classes rely on two core pillars:


🟢 Junior Level

30-Second Summary

Atomic classes in the java.util.concurrent.atomic package provide lock-free, thread-safe operations on individual variables and object references without thread blocking or OS-level context switching.

Unlike locks (synchronized, ReentrantLock), which suspend contending threads into BLOCKED or WAITING states via kernel calls, atomic classes rely on two core pillars:

  1. volatile memory semantics: Guarantees immediate cross-thread visibility and prevents instruction reordering (JMM Happens-Before).
  2. Hardware CAS (Compare-And-Swap) instructions: CPU-level atomic primitives that verify and update a memory location in a single indivisible transaction.

If another thread modifies the variable during an update, the operation does not block; instead, it spins in an optimistic CAS retry loop until the atomic update succeeds.


The Compound Operation Problem (count++)

A standard increment operation count++ is not atomic. At the bytecode and machine instruction levels, it breaks down into three distinct steps:

  1. Read: Load the current value from main memory/cache into a CPU register.
  2. Modify: Increment the value inside the CPU register.
  3. Write: Store the modified value back to cache and main memory.

When executed concurrently by multiple threads, an interleaved sequence of reads and writes triggers a Race Condition, resulting in lost updates:

// ❌ UNSAFE: Lost updates under concurrent access
private int unsafeCount = 0;
public void addUnsafe() { unsafeCount++; }

// ✅ THREAD-SAFE & LOCK-FREE: Indivisible CPU CAS primitive
private final AtomicInteger safeCount = new AtomicInteger(0);
public void addSafe() { safeCount.incrementAndGet(); }

Taxonomy of Atomic Classes in Java

Category Classes Purpose & Typical Use Case
Scalar Primitives AtomicBoolean, AtomicInteger, AtomicLong Atomic sequence counters, metrics, state flags
Reference Types AtomicReference<V> Atomic reference swaps of immutable domain models
Versioned References AtomicStampedReference<V>, AtomicMarkableReference<V> Solves the ABA problem by pairing references with integer stamps or boolean marks
Atomic Arrays AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray<E> Per-element volatile access and CAS operations
Field Updaters AtomicIntegerFieldUpdater<T>, AtomicReferenceFieldUpdater<T,V> Reflection-based CAS over plain volatile fields to minimize memory overhead
Scalable Accumulators LongAdder, LongAccumulator, DoubleAdder High-throughput striped counters under heavy thread contention

Idiomatic Java 21 Example

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class AtomicDemo {
    // 1. Single-execution initialization flag
    private final AtomicBoolean initialized = new AtomicBoolean(false);

    // 2. High-speed atomic counter
    private final AtomicInteger requestCounter = new AtomicInteger(0);

    // 3. Atomic publication of an immutable record
    public record AppConfig(String host, int port) {}
    private final AtomicReference<AppConfig> config = 
            new AtomicReference<>(new AppConfig("localhost", 8080));

    public void initOnce() {
        // Guaranteed to execute exactly once across all concurrent threads
        if (initialized.compareAndSet(false, true)) {
            System.out.println("Subsystem successfully initialized");
        }
    }

    public void handleRequest() {
        requestCounter.incrementAndGet(); // Thread-safe atomic increment
    }

    public void updateConfig(String newHost, int newPort) {
        // Functional, lock-free update using an optimistic CAS loop
        config.updateAndGet(old -> new AppConfig(newHost, newPort));
    }
}

🟡 Middle Level

CAS Mechanics & The Optimistic Retry Loop

The CAS operation accepts three parameters:

  1. Memory offset / address of the variable ($V$).
  2. Expected old value ($A$).
  3. New target value ($B$).

Semantics: The value $B$ is written to memory address $V$ if and only if the current value at $V$ exactly matches $A$. The check and write are executed as a single atomic CPU cycle.

Optimistic Spin Loop:

If CAS returns false (indicating a competing thread updated the variable first), the calling thread does not enter kernel sleep. Instead, it re-reads the updated value and retries the CAS operation:

// Underlying mechanics of getAndAddInt:
public final int getAndAddInt(Object o, long offset, int delta) {
    int current;
    do {
        current = getIntVolatile(o, offset); // 1. Volatile read of current state
    } while (!weakCompareAndSetInt(o, offset, current, current + delta)); // 2. Optimistic CAS write
    return current;
}

Architectural Evolution: Unsafe vs VarHandle (Java 9+)

Historically, atomic classes were implemented via sun.misc.Unsafe, a proprietary internal API operating on raw memory addresses.

Starting in Java 9 (JEP 193), atomic classes were re-architected on top of java.lang.invoke.VarHandle:

  • Provides strongly typed, verified access to fields and array elements.
  • Maps directly to JMM access modes without reflection performance penalties.
  • Fully optimized by the HotSpot C2 JIT compiler into inline machine assembly.

Memory Access Modes in Modern Atomic Classes

Modern atomic classes expose fine-grained control over memory barriers:

AtomicInteger atom = new AtomicInteger(0);

// 1. Full Volatile Write: Enforces StoreStore + StoreLoad barriers
atom.set(42);

// 2. Release / Ordered Write: Generates StoreStore barrier, skips expensive StoreLoad
atom.lazySet(42);

// 3. Full Bi-directional Barrier: Acquire + Release semantics
atom.compareAndSet(42, 100);

// 4. Weak CAS: Allows spurious failures, avoids full fencing on certain architectures
atom.weakCompareAndSetVolatile(100, 200);

Why Use lazySet()?

lazySet() writes the value with Ordered / Release semantics. It guarantees that all preceding writes by the thread are flushed to memory before this write (via a StoreStore barrier), but it omits the expensive StoreLoad CPU barrier. Other threads will observe the value with a negligible hardware delay (eventual visibility). This provides a massive throughput boost in scenarios like ring buffers (e.g., LMAX Disruptor) or clearing references in lock-free queues.


🔴 Senior Level

Hardware-Level CAS: x86-64 vs. ARM64 / RISC-V

The physical implementation of CAS differs fundamentally between CPU architectures:

  1. x86-64 Architecture (CISC):
    • Utilizes the single instruction LOCK CMPXCHG.
    • The LOCK prefix asserts exclusive ownership over the target CPU cache line via the cache coherency protocol (MESI / MOESI). The cache line is locked during the execution of the instruction, ensuring hardware-level atomicity.
  2. ARM64 and RISC-V Architectures (RISC):
    • Do not provide a monolithic atomic CAS instruction.
    • Employ a pair of instructions: LL/SC (Load-Linked / Store-Conditional):
      • LDXR (Load Exclusive): Reads the memory value and registers an exclusive hardware monitor on the cache line.
      • STXR (Store Exclusive): Attempts to write the new value; succeeds only if no other processor core has invalidated or written to that monitored cache line since the LDXR.

The ABA Problem & Version Stamps

The ABA problem is an inherent vulnerability in lock-free data structures (such as Treiber lock-free stacks or Michael-Scott queues):

1. Thread 1 reads top node 'A' (which references 'B').
2. Thread 1 stalls before executing CAS.
3. Thread 2 pops 'A', pops 'B', and deallocates them.
4. Thread 2 allocates a new node that happens to be assigned memory address 'A', and pushes it to the stack.
5. Thread 1 resumes and executes CAS(A, B).
   Memory address matches ('A' == 'A')!
   CAS succeeds, but the stack is corrupted because 'B' points to invalid memory!

The Solution: AtomicStampedReference

AtomicStampedReference binds an object reference to an integer version stamp (revision number):

AtomicStampedReference<Node> stackTop = new AtomicStampedReference<>(initialNode, 0);

int[] stampHolder = new int[1];
Node currentTop = stackTop.get(stampHolder);
int currentStamp = stampHolder[0];

// CAS succeeds ONLY if BOTH the reference AND the version stamp match:
boolean success = stackTop.compareAndSet(
    currentTop, 
    newTop, 
    currentStamp, 
    currentStamp + 1
);

High Contention Bottleneck: AtomicLong vs LongAdder

Under extreme write contention across dozens of threads (e.g., global metrics, rate limiters), AtomicLong suffers severe performance degradation:

  • Every thread executes LOCK CMPXCHG on the exact same 64-byte CPU cache line.
  • This triggers Cache Bouncing (Cache Line Ping-Pong): the cache line is continuously invalidated and transferred across CPU cores via the interconnect bus (QPI/UPI), saturating hardware bandwidth.
  • Threads spin fruitlessly in CAS retry loops, burning 100% CPU.
                    Cache Line Bouncing in AtomicLong
        CPU Core 1               CPU Core 2               CPU Core 3
     ┌──────────────┐         ┌──────────────┐         ┌──────────────┐
     │ LOCK CMPXCHG │         │ LOCK CMPXCHG │         │ LOCK CMPXCHG │
     └───────┬──────┘         └───────┬──────┘         └───────┬──────┘
             │                        │                        │
             └────────────► [ Cache Line: value ] ◄────────────┘
                     (Continuous Cache Invalidation)

Architecture of LongAdder (Striped64):

LongAdder eliminates central memory contention through dynamic sharding:

  1. Under zero contention, threads update a single volatile long base field.
  2. Upon the first CAS collision, it initializes an internal array: volatile Cell[] cells.
  3. Each thread hashes to a separate Cell slot based on its thread-local probe (Thread.currentThread().threadLocalRandomProbe).
  4. The Cell class is annotated with @jdk.internal.vm.annotation.Contended, which injects 128 bytes of padding around the field to eliminate False Sharing by guaranteeing each cell occupies its own CPU cache line.
  5. Calling sum() aggregates base and all active Cell values on demand.

Memory Optimization: AtomicIntegerFieldUpdater

An AtomicInteger object requires 16 bytes (with Compressed OOPs) or 24 bytes of heap memory as a standalone object header and payload wrapper.

When an application tracks millions of domain objects (e.g., active socket sessions or in-memory game entities), allocating dedicated atomic wrappers consumes significant memory:

// ❌ Wasteful: 1,000,000 entities * 24 bytes = 24 MB heap overhead for wrapper objects
public class Session {
    private final AtomicInteger state = new AtomicInteger(0);
}

// ✅ Optimized: Raw volatile primitive (4 bytes) + static singleton updater
public class OptimizedSession {
    private static final AtomicIntegerFieldUpdater<OptimizedSession> STATE_UPDATER =
            AtomicIntegerFieldUpdater.newUpdater(OptimizedSession.class, "state");

    // Field MUST be declared volatile and cannot be private!
    volatile int state = 0;

    public void activate() {
        STATE_UPDATER.compareAndSet(this, 0, 1);
    }
}

4 Tricky Questions

1. What is the difference between set(), lazySet(), and compareAndSet() in AtomicInteger at the memory barrier level?

Answer:

  • set(v): Performs a full volatile write. The compiler/JVM emits a StoreStore barrier prior to the store and a heavyweight StoreLoad barrier immediately following it. This forces the CPU Store Buffer to drain, making the write visible to all other cores immediately.
  • lazySet(v): Performs an ordered/release write. It emits a StoreStore barrier (ensuring prior memory stores complete before this write) but omits the StoreLoad barrier. The write executes with minimal CPU pipeline stalling, but cross-core visibility is slightly delayed (eventual consistency).
  • compareAndSet(expect, update): Enforces a full bidirectional memory barrier (Acquire + Release). No preceding or succeeding memory operations can be reordered across the CAS boundary.

2. What is the ABA problem, in which scenarios is it critical, and is it dangerous for a simple AtomicInteger counter?

Answer: The ABA problem occurs when a memory location originally holding value $A$ is changed to $B$ and then restored back to $A$. A thread executing CAS(A, C) perceives that the value has remained unchanged and succeeds, even though intermediate state transitions took place.

  • Harmless for scalar counters: In a plain numeric counter (AtomicInteger), the intermediate states do not invalidate the arithmetic invariant; the number $A$ is mathematically indistinguishable from another $A$.
  • Catastrophic in lock-free memory management: In lock-free node-based structures (e.g., memory reuse in Treiber stacks), if node $A$ is popped, freed, and re-allocated at the same memory address, its internal next pointer may have changed. A thread verifying only pointer address equality will link corrupt or obsolete memory structures. The solution is AtomicStampedReference, which pairs the pointer with a monotonically increasing integer revision stamp.

3. Why does calling LongAdder.sum() during active concurrent writes not guarantee an exact point-in-time snapshot?

Answer: LongAdder.sum() does not acquire a global lock, nor does it freeze writer threads. It simply iterates through the base field and the Cell[] array, accumulating their values. If a concurrent thread increments a cell that sum() has already processed, or if a cell moves during internal re-hashing, the computed total will not represent an instantaneous global state snapshot. LongAdder is engineered specifically for high-throughput metrics collection where eventual accumulation speed takes priority over exact point-in-time snapshot consistency.


4. How can you atomically update two interrelated fields without locks (synchronized/Lock)?

Answer: You cannot use two separate AtomicInteger variables; updating them sequentially creates a race window between the first and second updates.
The correct lock-free idiom:

  1. Encapsulate both fields inside an immutable data holder (in modern Java, a record is ideal).
  2. Store the record reference inside an AtomicReference<ImmutableState>.
  3. Perform an atomic state swap using updateAndGet(): ```java public record Coordinate(int x, int y) {} private final AtomicReference pos = new AtomicReference<>(new Coordinate(0, 0));

public void move(int dx, int dy) { pos.updateAndGet(curr -> new Coordinate(curr.x() + dx, curr.y() + dy)); } ```


🎯 Interview Cheat Sheet

Core Concepts Matrix

| Feature | Scalar Atomics (AtomicInteger) | Scalable Adders (LongAdder) | Intrinsic Lock (synchronized) | | :— | :— | :— | :— | | Concurrency Model | Lock-free (CAS loop) | Lock-free sharded cells | Blocking monitor lock | | Contention Profile | Low-to-medium contention | High write contention | General mutual exclusion | | Cache Line Behavior| Cache Bouncing on write hotspots| Isolated per cell via @Contended | Monitor state transitions | | Memory Footprint | 16–24 bytes per instance | Dynamically scales array | 16-byte object header | | Multi-field Atomicity| Single field only | Single primitive counter | Arbitrary statement blocks |


Key Takeaways

  • Non-blocking primitives: Atomic classes achieve lock-free thread safety using CPU hardware CAS (LOCK CMPXCHG on x86, LL/SC on ARM) and volatile visibility.
  • Check-Then-Act anti-pattern: Never combine separate atomic calls like if (atom.get() == 0) atom.set(1);—always use compareAndSet().
  • Scaling writes: When write contention on an AtomicLong causes CPU thrashing, replace it with LongAdder.
  • Heap reduction: For millions of entity instances, replace per-object AtomicInteger instances with AtomicIntegerFieldUpdater over a primitive volatile int.

Red Flags & Anti-Patterns to Avoid

  • ❌ Assuming Atomics are always faster than synchronized: Under massive write contention, scalar atomics spin in CAS loops consuming 100% CPU, whereas thread parking in locks can yield higher overall system efficiency.
  • ❌ Using AtomicReference to mutate mutable objects: AtomicReference only ensures thread-safe swapping of the reference pointer; mutating internal fields of a referenced object is not thread-safe.
  • ❌ Omitting volatile on fields used by AtomicIntegerFieldUpdater: The target field must be declared volatile and cannot be private, or the updater factory will throw an IllegalArgumentException.