Question 8 · Section 9

What are Atomic classes?

A regular count++ operation is not thread-safe:

Language versions: English Russian Ukrainian

Junior Level

Basic Understanding

Atomic classes are a set of classes from java.util.concurrent.atomic that provide thread-safe operations without locks (lock-free).

Lock-free = no mutex, no thread blocking. Uses a CAS cycle: read the value, compute the new one, try to write via CAS. If another thread changed the value — CAS returns false, repeat the cycle.

Why are they needed?

A regular count++ operation is not thread-safe:

// NOT safe!
public class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++; // 3 operations: read → modify → write
        // Two threads may read the same value!
    }
}

Solution with Atomic

// Safe!
public class SafeCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // Atomic operation
    }
}

Main Atomic Classes

Class Type Example Usage
AtomicBoolean boolean State flag
AtomicInteger int Counter
AtomicLong long Counter (64-bit)
AtomicReference<T> Object Thread-safe reference
AtomicIntegerArray int[] Array of counters
LongAdder long High-load counter

Usage Examples

// AtomicInteger
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();   // ++i (returns new value)
counter.getAndIncrement();   // i++ (returns old value)
counter.addAndGet(5);        // i += 5
counter.compareAndSet(5, 10);// If i==5, set to 10

// AtomicBoolean
AtomicBoolean flag = new AtomicBoolean(false);
flag.set(true);
boolean oldValue = flag.getAndSet(false);

// AtomicReference
AtomicReference<String> ref = new AtomicReference<>("initial");
ref.compareAndSet("initial", "updated");

Mechanism: CAS + volatile

Atomic classes work on two pillars:

  1. volatile field — guarantees visibility of changes
  2. CAS (Compare-And-Swap) — atomic update
// Simplified AtomicInteger implementation
public class AtomicInteger {
    private volatile int value; // volatile for visibility

    public final int incrementAndGet() {
        for (;;) { // Infinite loop
            int current = get();         // 1. Read
            int next = current + 1;      // 2. Compute
            if (compareAndSet(current, next)) { // 3. CAS — update
                return next;             // Success!
            }
            // CAS failed — someone else updated, try again
        }
    }
}

Middle Level

CAS (Compare-And-Swap) in Detail

CAS is a CPU instruction that atomically:

  1. Compares the value at an address with the expected value
  2. If equal — writes the new value
  3. If not equal — does nothing
// CAS pseudocode
boolean CAS(memoryAddress, expectedValue, newValue) {
    if (*memoryAddress == expectedValue) {
        *memoryAddress = newValue;
        return true;
    }
    return false;
}

On x86 this is the LOCK CMPXCHG instruction.

CMPXCHG (CoMPare and eXCHanGe) — x86 instruction for CAS operation. On ARM — ldxr/stxr, on RISC-V — lr.w/sc.w (Load Reserved / Store Conditional).

Reference Updates

public class AtomicReferenceDemo {
    private AtomicReference<List<String>> list = new AtomicReference<>(new ArrayList<>());

    public void addItem(String item) {
        list.updateAndGet(currentList -> {
            // Create a new copy (immutable update)
            List<String> newList = new ArrayList<>(currentList);
            newList.add(item);
            return newList;
        });
    }
}

VarHandle (Java 9+)

Starting with Java 9, Atomic classes are rewritten using VarHandle:

VarHandle (Java 9, JEP 193) — replacement for sun.misc.Unsafe for atomic operations. Safer (no direct memory access), but with the same capabilities. The API of Atomic classes remained backward compatible — no need to change your code.

// Instead of sun.misc.Unsafe — typed and safe API
public class AtomicInteger {
    private static final VarHandle VALUE;
    private volatile int value;

    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
            VALUE = l.findVarHandle(AtomicInteger.class, "value", int.class);
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }
}

Different Memory Guarantee Levels

AtomicLong counter = new AtomicLong(0);

// Different write methods:
counter.set(100);          // Volatile write — full visibility
counter.lazySet(100);      // Relaxed visibility: doesn't set StoreLoad barrier, write may become visible with a delay. Faster!
counter.compareAndSet(0, 100); // CAS with full barrier
Method Guarantee Speed
set(v) Full visibility immediately Standard
lazySet(v) Eventual visibility Faster
compareAndSet Full barrier Standard
weakCompareAndSet May return false spuriously (even if the value matches) on some architectures. This is not a bug — allowed by the specification for optimization on CPUs with weak memory models (ARM, RISC-V). Faster

LongAdder for High Contention

With hundreds of threads updating a single counter, AtomicLong starts to slow down (CAS constantly fails).

// BAD under high contention:
AtomicLong counter = new AtomicLong(0);
// Every thread constantly "misses" on CAS

// GOOD under high contention:
LongAdder counter = new LongAdder();
counter.increment(); // Distributes across cells
long total = counter.sum(); // Sums all cells

How LongAdder works:

  • Instead of one variable — an array of Cells
  • Different threads update different cells
  • On sum(), values are summed up

When Atomic classes are NOT suitable

  1. Compound operations on multiple fields: need to atomically update two fields — AtomicInteger won’t help, need synchronized or AtomicReference with an immutable object
  2. High contention (100+ threads): CAS cycle wastes CPU — better to use LongAdder
  3. Long computations between read and write: if a lot of time passes between read and write — CAS will constantly fail, better to use Lock

Senior Level

Under the Hood: CAS Implementation

At the x86 CPU level:

; LOCK CMPXCHG — atomic operation
; rax = expected, rcx = newValue, [memory] = actual value

lock cmpxchg [memory], rcx
; If [memory] == rax:
;   [memory] = rcx
;   ZF = 1 (success)
; Else:
;   rax = [memory]
;   ZF = 0 (failure)

The lock prefix locks the memory bus for the duration of the operation, ensuring atomicity on multiprocessor systems.

ABA Problem

A classic CAS problem:

Thread 1: read value A
Thread 2: changed A → B → A (value returned!)
Thread 1: CAS(A, new_value) — SUCCESS! But the state changed!

Solution: AtomicStampedReference

// Stores value + version (stamp)
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

int[] stampHolder = new int[1];
String value = ref.get(stampHolder);
int stamp = stampHolder[0];

// CAS with version check
boolean success = ref.compareAndSet("A", "B", stamp, stamp + 1);

AtomicMarkableReference

// Stores value + boolean flag
AtomicMarkableReference<Node> ref = new AtomicMarkableReference<>(node, false);

// Mark node as "removed"
ref.compareAndSet(node, node, false, true);

LongAdder Internal Structure

// Simplified
public class LongAdder {
    // Base value (used under low contention)
    volatile long base;

    // Cell array (used under high contention)
    volatile Cell[] cells;

    static final class Cell {
        @Contended // Padding to avoid False Sharing
        volatile long value;
    }

    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // Base CAS failed — use Cell
            // Each thread hashes to its own cell
            // ...
        }
    }

    public long sum() {
        long sum = base;
        for (Cell a : cells) {
            if (a != null) sum += a.value;
        }
        return sum;
    }
}

Performance and Highload

Benchmark (approximate, 100 threads)

Operation Time per operation Note
synchronized ~5000 ns Context switching
AtomicLong ~100 ns CAS contention
LongAdder ~10 ns Distributed cells

False Sharing

// Problem: array of Atomic elements in one cache line
AtomicInteger[] counters = new AtomicInteger[16];
// counters[0] and counters[1] may be in the same cache line
// Writing to counters[0] invalidates counters[1]

Solution — @Contended inside LongAdder:

@jdk.internal.vm.annotation.Contended
volatile long value; // 128 bytes of padding around

AtomicIntegerFieldUpdater — Memory Savings

If you have millions of objects and each needs an atomic counter:

// BAD: millions of additional AtomicInteger objects in memory
class User {
    AtomicInteger loginAttempts = new AtomicInteger(0); // +24 bytes per object
}

// GOOD: updater works with a regular int field
class User {
    volatile int loginAttempts = 0; // 4 bytes

    private static final AtomicIntegerFieldUpdater<User> UPDATER =
        AtomicIntegerFieldUpdater.newUpdater(User.class, "loginAttempts");

    public void increment() {
        UPDATER.incrementAndGet(this);
    }
}

Diagnostics

CAS Failures in Profiler

# Java Flight Recorder — look for contention events
java -XX:StartFlightRecording=filename=rec.jfr MyApp

If lots of time in incrementAndGet — high contention.

Bytecode Analysis

javap -c -p AtomicInteger.class

getAndAddInt instructions — native, translate to lock cmpxchg.

Best Practices

  1. Atomic* for simple counters and flags under moderate contention
  2. LongAdder for high-load counters (metrics, statistics)
  3. AtomicReference with updateAndGet for immutable updates
  4. AtomicStampedReference when the ABA problem matters
  5. synchronized for complex operations (multiple fields at once)
  6. @Contended for arrays of atomic counters (requires JVM flag)
  7. Avoid AtomicLong as ID generator at 1000+ RPS — use range allocation

Interview Cheat Sheet

Must know:

  • Atomic classes provide lock-free thread safety via CAS + volatile
  • incrementAndGet = spin cycle: read → modify → CAS retry until success
  • On x86, CAS = lock cmpxchg instruction with lock prefix (memory bus lock)
  • Java 9+ rewritten to use VarHandle (safe replacement for sun.misc.Unsafe, JEP 193)
  • ABA problem: value changed A→B→A, CAS “won’t notice”; solution — AtomicStampedReference
  • LongAdder solves contention at 100+ threads: distributes across Cell[] array, sums on sum()
  • lazySet() — relaxed visibility (eventual), faster than set() — doesn’t set StoreLoad barrier

Frequent follow-up questions:

  • Why does AtomicLong slow down under high contention? — CAS constantly fails, threads spin in a cycle, wasting CPU
  • How does LongAdder differ from AtomicLong? — LongAdder: array of cells, each thread writes to its own; AtomicLong: one variable, all compete
  • What is the ABA problem? — Thread 1 read A, Thread 2 changed A→B→A, Thread 1: CAS(A) — success, but state changed
  • When to use AtomicReference? — For atomic replacement of immutable objects (e.g., configuration update)

Red flags (do NOT say):

  • “Atomic classes use locks” — no, they’re lock-free via CAS
  • “CAS is atomic on any architecture” — on 32-bit JVMs, AtomicLong may use an internal lock
  • “lazySet() guarantees immediate visibility” — no, eventual consistency, without StoreLoad barrier
  • “LongAdder is always better than AtomicLong” — no, LongAdder is faster only under high contention; for normal cases, AtomicLong

Related topics:

  • [[9. What is CAS (Compare-And-Swap)]] — foundation of all Atomic classes
  • [[1. What is the difference between synchronized and volatile]] — volatile for visibility + CAS for atomicity
  • [[3. What is visibility problem]] — volatile field inside Atomic ensures visibility
  • [[7. What is reentrant lock]] — AQS (ReentrantLock’s foundation) also uses CAS