[THREADING 101] 06 – Atomic Classes and Operations

[

The Problem with Locks

Locks work, but they have costs:

  • Contention — threads waiting for a lock do nothing useful.
  • Context switches — the OS moves blocked threads on and off the CPU.
  • Priority inversion — a low-priority thread holding a lock blocks a high-priority thread.
  • Deadlock risk — multiple locks can create circular dependencies.

For simple operations — incrementing a counter, updating a reference — there’s a better way.

Compare-And-Swap (CAS)

CAS is the foundation of all lock-free programming in Java. It’s a single CPU instruction that atomically:

  1. Reads the current value at a memory location
  2. Compares it to an expected value
  3. If they match, writes a new value
  4. Returns whether the swap succeeded

In pseudocode:

boolean CAS(memoryLocation, expectedValue, newValue):
    if memoryLocation.value == expectedValue:
        memoryLocation.value = newValue
        return true
    else:
        return false

This entire operation is a single atomic CPU instruction (cmpxchg on x86, ldxr/stxr on ARM). No lock is needed because the hardware guarantees atomicity.

The java.util.concurrent.atomic Package

Java wraps CAS operations in a set of atomic classes:

Scalar Types

ClassWrapsKey Use
AtomicBooleanbooleanFlags, one-shot triggers
AtomicIntegerintCounters, accumulators
AtomicLonglongCounters, sequence numbers
AtomicReference<T>Object referenceLock-free data structures

Array Types

ClassWraps
AtomicIntegerArrayint[] — each element is individually atomic
AtomicLongArraylong[]
AtomicReferenceArray<T>T[]

Stamped/Marked References

ClassPurpose
AtomicMarkableReference<T>Reference + boolean mark (for soft-delete flags)
AtomicStampedReference<T>Reference + int stamp (solves ABA problem)

Accumulators (Java 8+)

ClassPurpose
LongAdderHigh-throughput counter (better than AtomicLong under contention)
LongAccumulatorGeneralized accumulation with custom function
DoubleAdderHigh-throughput double accumulator
DoubleAccumulatorGeneralized double accumulation

AtomicInteger — Key Methods

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger counter = new AtomicInteger(0);
MethodDescriptionReturn
get()Read current valueCurrent value
set(int value)Set value (volatile write)void
getAndSet(int value)Set and return previousPrevious value
compareAndSet(int expect, int update)CAS operationtrue if successful
getAndIncrement()Post-increment (i++)Previous value
incrementAndGet()Pre-increment (++i)New value
getAndDecrement()Post-decrement (i–)Previous value
decrementAndGet()Pre-decrement (–i)New value
getAndAdd(int delta)Add and return previousPrevious value
addAndGet(int delta)Add and return newNew value
getAndUpdate(IntUnaryOperator)Apply function, return previousPrevious value
updateAndGet(IntUnaryOperator)Apply function, return newNew value
getAndAccumulate(int x, IntBinaryOperator)Accumulate with valuePrevious value
accumulateAndGet(int x, IntBinaryOperator)Accumulate with valueNew value

Usage Examples

AtomicInteger counter = new AtomicInteger(0);

// Simple increment
counter.incrementAndGet(); // 1

// Conditional update
boolean success = counter.compareAndSet(1, 10); // true, counter is now 10

// Custom update function
counter.updateAndGet(current -> current * 2); // 20

// Accumulate
counter.accumulateAndGet(5, Integer::max); // max(20, 5) = 20
counter.accumulateAndGet(50, Integer::max); // max(20, 50) = 50

AtomicReference — Key Methods

import java.util.concurrent.atomic.AtomicReference;

AtomicReference<String> ref = new AtomicReference<>("initial");
MethodDescription
get()Read current reference
set(V value)Set reference (volatile write)
getAndSet(V value)Swap and return previous
compareAndSet(V expect, V update)CAS on reference identity
getAndUpdate(UnaryOperator<V>)Apply function, return previous
updateAndGet(UnaryOperator<V>)Apply function, return new

Important: CAS Compares Identity, Not Equality

AtomicReference<String> ref = new AtomicReference<>("hello");

String current = ref.get();
// CAS uses == (reference identity), not .equals()
ref.compareAndSet(current, "world"); // Works — same reference

ref.compareAndSet(new String("world"), "!"); // FAILS — different object, even if .equals() is true

LongAdder vs AtomicLong

Under high contention (many threads hammering the same counter), AtomicLong degrades because every failed CAS triggers a retry. LongAdder solves this by spreading updates across multiple internal cells:

import java.util.concurrent.atomic.LongAdder;

LongAdder adder = new LongAdder();

// Multiple threads call:
adder.increment();    // Spreads across internal cells
adder.add(5);

// When you need the total:
long total = adder.sum(); // Aggregates all cells

Performance Comparison

ScenarioAtomicLongLongAdder
Low contention (few threads)FastSame (slight overhead)
High contention (many threads)Degrades — CAS retriesScales linearly
Reading the valueO(1)O(cells) — must sum
MemoryFixedGrows with contention

Rule of thumb: Use AtomicLong when you read the value frequently. Use LongAdder when updates are frequent and reads are rare (like metrics/counters).

AtomicStampedReference — Solving the ABA Problem

The ABA Problem

CAS checks: “Is the value still what I expect?” But what if the value was A, changed to B, and changed back to A? CAS sees A and thinks nothing happened — but the intermediate state change might matter.

Thread 1: reads A
Thread 2: changes A → B → A
Thread 1: CAS(expected=A, new=C) → succeeds! (but state was modified)

Solution: Stamp the Reference

import java.util.concurrent.atomic.AtomicStampedReference;

AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

// Read value AND stamp
int[] stampHolder = new int[1];
String current = ref.get(stampHolder);
int currentStamp = stampHolder[0];

// CAS checks BOTH reference AND stamp
boolean success = ref.compareAndSet(
    current,           // expected reference
    "B",              // new reference
    currentStamp,     // expected stamp
    currentStamp + 1  // new stamp
);

Even if the reference returns to the same value, the stamp will be different, and CAS will correctly detect the change.

Building Thread-Safe Patterns with Atomics

Lock-Free Counter

public class MetricsCounter {
    private final AtomicLong requestCount = new AtomicLong(0);
    private final AtomicLong errorCount = new AtomicLong(0);
    private final LongAdder totalLatency = new LongAdder();

    public void recordRequest(long latencyMs, boolean isError) {
        requestCount.incrementAndGet();
        totalLatency.add(latencyMs);
        if (isError) {
            errorCount.incrementAndGet();
        }
    }

    public double getErrorRate() {
        long requests = requestCount.get();
        return requests == 0 ? 0 : (double) errorCount.get() / requests;
    }

    public double getAverageLatency() {
        long requests = requestCount.get();
        return requests == 0 ? 0 : (double) totalLatency.sum() / requests;
    }
}

Lock-Free Lazy Initialization

public class AtomicLazyInit<T> {
    private final AtomicReference<T> ref = new AtomicReference<>();
    private final Supplier<T> factory;

    public AtomicLazyInit(Supplier<T> factory) {
        this.factory = factory;
    }

    public T get() {
        T instance = ref.get();
        if (instance != null) {
            return instance;
        }
        
        T newInstance = factory.get();
        if (ref.compareAndSet(null, newInstance)) {
            return newInstance; // We won the race
        } else {
            return ref.get(); // Another thread initialized first
        }
    }
}

Lock-Free Maximum Tracker

public class MaxTracker {
    private final AtomicLong max = new AtomicLong(Long.MIN_VALUE);

    public void observe(long value) {
        long current;
        do {
            current = max.get();
            if (value <= current) return; // Not a new max
        } while (!max.compareAndSet(current, value));
    }

    public long getMax() {
        return max.get();
    }
}

When to Use Atomic Classes

Use CaseRecommended Class
Simple counterAtomicInteger / AtomicLong
High-contention counterLongAdder
Boolean flagAtomicBoolean
Shared mutable referenceAtomicReference<T>
Lock-free data structuresAtomicReference<T> + CAS loop
Counter per array indexAtomicIntegerArray
ABA-sensitive algorithmsAtomicStampedReference<T>
Soft-delete markersAtomicMarkableReference<T>
Custom accumulationLongAccumulator

Summary

  • Atomic classes use CAS (Compare-And-Swap) — a single CPU instruction — instead of locks.
  • compareAndSet is the core operation: “update only if the value is what I expect.”
  • Under low contention, atomics are faster than locks. Under high contention, LongAdder scales better than AtomicLong.
  • CAS loops (read → compute → CAS → retry if failed) are the standard pattern for lock-free updates.
  • The ABA problem is solved by AtomicStampedReference which checks both value and version.
  • Atomics provide visibility guarantees equivalent to volatile.

References

About the author

Maksim

I build AI-powered products and lead engineering teams. I've launched platforms from zero to millions of users and learned most lessons the hard way. I write about the gap between engineering theory and practice, what actually matters when building products, and the decisions that shape teams and systems.

Add Comment

By Maksim

Maksim

Get in touch

Reach out if you want to discuss engineering leadership, collaborate on something interesting, or suggest topics you'd like me to write about.