Atomic Classes

The java.util.concurrent.atomic classes like AtomicInteger and AtomicReference provide lock-free, atomic read-modify-write operations built on hardware compare-and-set, avoiding the overhead of locking for simple shared counters and references.

java.util.concurrent.atomic provides a family of classes — AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference<V>, and array/field-updater variants — that support atomic compound operations on a single variable, such as incrementing a counter or conditionally updating a reference, without requiring explicit locking. Internally, most of these are built on a hardware-level compare-and-set (CAS) instruction, which is why they can offer atomicity with substantially lower overhead than synchronized or ReentrantLock for the narrow case of updating one variable.

Why Atomic Classes Exist

Earlier topics established that plain int/long fields (even volatile ones) don't make compound operations like count++ atomic, and that synchronized/Lock, while correct, impose locking overhead and block contending threads. Atomic classes fill the gap: for the common case of a single variable that needs atomic read-modify-write updates, they provide that atomicity directly, typically without ever blocking a thread.

AtomicInteger and AtomicLong

AtomicInteger and AtomicLong wrap an int or long value and expose atomic operations on it: get(), set(), incrementAndGet(), getAndIncrement(), decrementAndGet(), addAndGet(delta), getAndAdd(delta), updateAndGet(unaryOp), and more.

incrementAndGet() performs the entire read-modify-write as one atomic operation, so no interleaving between threads can cause a lost update, unlike a plain (even volatile) int's count++.

The getAndX vs XAndGet naming convention consistently distinguishes 'return the value before the operation' from 'return the value after the operation' across the whole atomic API.

import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private final AtomicInteger count = new AtomicInteger(0);

    void increment() { count.incrementAndGet(); } // atomic, no lock needed
    int get() { return count.get(); }
}
// Two threads each calling increment() 100_000 times reliably total 200_000.
AtomicInteger a = new AtomicInteger(10);
System.out.println(a.getAndIncrement()); // 10 (returns OLD value, then increments)
System.out.println(a.incrementAndGet()); // 12 (increments, then returns NEW value)
System.out.println(a.getAndAdd(5));      // 12 (returns OLD value, then adds 5)
System.out.println(a.get());             // 17

AtomicBoolean and AtomicReference

AtomicBoolean provides the same atomic-update pattern for a boolean flag. AtomicReference<V> generalizes the idea to any object reference, letting a reference field be atomically read, set, or compare-and-swapped, which is especially useful for lock-free algorithms that swap out an entire immutable object atomically rather than mutating fields in place.

Because Point is immutable, swapping the reference atomically is enough to publish a fully-formed new state with no partial-update window — a common lock-free pattern.

import java.util.concurrent.atomic.AtomicReference;

record Point(int x, int y) {}

class PositionTracker {
    private final AtomicReference<Point> position = new AtomicReference<>(new Point(0, 0));

    void moveTo(int x, int y) {
        position.set(new Point(x, y)); // atomic reference swap
    }

    Point getPosition() { return position.get(); }
}

The ABA Problem

Compare-and-set only checks whether the current value equals the expected value — it cannot tell whether that value changed and then changed back to the same value in between, a scenario known as the ABA problem. If a thread reads value A, gets suspended, and while suspended another thread changes the value from A to B and back to A again, the first thread's compareAndSet(A, newValue) still succeeds, even though the value did briefly change — which can be a genuine problem for algorithms where the identity of an intermediate state matters (e.g. certain lock-free stack/list implementations), not just its final equality.

AtomicStampedReference (and the related AtomicMarkableReference) exist specifically to defend against ABA, by requiring an auxiliary version stamp or boolean mark to also match, not just the value itself.

For most everyday uses of AtomicInteger/AtomicLong/AtomicReference (counters, simple flags, single-value publication), the ABA problem does not arise as a practical concern — it matters specifically for certain lock-free data structure algorithms (like some CAS-based stack or queue implementations) where an intermediate state's identity, not just its final value, affects correctness.

import java.util.concurrent.atomic.AtomicStampedReference;

// Pairs a reference with an integer "stamp" that must also match for CAS to succeed,
// letting code detect that the value changed and changed back.
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String current = ref.get(stampHolder); // current = "A", stampHolder[0] = 0

// Even if another thread does: A -> B -> A, bumping the stamp each time,
// this thread's compareAndSet("A", "next", 0, 1) fails if the stamp no longer matches 0,
// correctly detecting the intermediate change that a plain compareAndSet("A", "next") would miss.

Compare-and-Set (CAS)

compareAndSet(expectedValue, newValue) is the foundational operation nearly everything else in this package is built on: it atomically checks whether the current value equals expectedValue, and if so replaces it with newValue, returning true; if the current value doesn't match expectedValue (because another thread changed it first), it does nothing and returns false. This is a hardware-supported primitive (typically an atomic CPU instruction such as compare-and-swap) rather than something implemented via locking.

The second call fails harmlessly because the current value no longer matches the expected value — no exception, no corruption, just a false return the caller can react to (often by retrying with the new current value).

AtomicInteger balance = new AtomicInteger(100);

boolean success = balance.compareAndSet(100, 150); // expects 100, sets to 150
System.out.println(success);              // true, balance is now 150

boolean failed = balance.compareAndSet(100, 200);   // expects 100, but current value is 150
System.out.println(failed);               // false, balance unchanged (still 150)

The Retry-Loop Pattern Behind CAS-Based Updates

Methods like incrementAndGet() are typically implemented internally as a loop: read the current value, compute the new value, attempt compareAndSet(old, new), and retry from the top if that CAS failed because another thread updated the value in between. This is exactly what updateAndGet() and accumulateAndGet() expose for custom logic.

updateAndGet() internally retries the compare-and-set loop as needed if concurrent updates interfere, so the lambda may be invoked more than once per call under contention -- it should therefore be a pure function with no side effects.

Because the update function passed to updateAndGet()/accumulateAndGet() can be invoked multiple times if the CAS retries, it must be free of side effects — the same requirement applies conceptually to functions passed into Stream operations for the same underlying reason.

AtomicInteger max = new AtomicInteger(Integer.MIN_VALUE);

void observe(int candidate) {
    max.updateAndGet(current -> Math.max(current, candidate)); // atomic "keep the larger value" update
}

Limitations: Not Automatically Lock-Free, and Single-Variable Only

Atomic classes only guarantee atomicity for operations on that one variable. If an invariant spans two or more variables (e.g. 'x and y must always be updated together, and observed together'), atomic classes cannot enforce that jointly — updating two AtomicIntegers 'atomically together' is not something these classes provide; that requires a lock protecting both.

It's also a common misconception that 'atomic' automatically means 'lock-free' in every JVM implementation. Atomicity (the operation appears indivisible) and lock-freedom (no thread ever has to wait for another thread to make progress) are related but separate properties. Most CAS-based operations on modern hardware are lock-free in practice, but the JLS/API does not mandate a specific implementation strategy, and under very high contention even CAS-based retry loops can spend significant time retrying (though this is a performance characteristic, not a correctness or lock-holding issue).

Each addAndGet() call is individually atomic, but a reader could observe an inconsistent intermediate state where x reflects the move and y does not yet — protecting the pair as a unit requires a lock, or combining them into a single AtomicReference to an immutable Point object.

class Point2D {
    private final AtomicInteger x = new AtomicInteger();
    private final AtomicInteger y = new AtomicInteger();

    void move(int dx, int dy) {
        x.addAndGet(dx); // atomic individually...
        y.addAndGet(dy); // ...but NOT atomic as a combined (x, y) update
        // another thread could observe x updated but y not yet updated
    }
}

Relation to volatile

Atomic classes internally use a volatile-backed value (or equivalent memory semantics) for their get()/set() operations, meaning plain get() and set() calls have the same visibility and ordering guarantees as reading/writing a volatile field. What atomic classes add on top of that is the missing piece volatile alone lacks: atomic compound operations like compareAndSet() and incrementAndGet(), so the read-modify-write sequence itself is indivisible, not just each individual read or write.

AtomicInteger can be thought of as 'volatile int, plus atomic compound operations' -- it doesn't give up any of volatile's guarantees, it adds to them.

volatile field:        visible reads/writes,  NOT atomic compound ops
AtomicInteger, etc.:    visible reads/writes,  atomic compound ops (CAS-based)

Practical Usage: Counters, Flags, and Lock-Free Structures

Common uses: shared counters/statistics (request counts, retry counts) updated by many threads; a boolean 'started'/'shutdown' flag needing atomic check-and-set semantics (compareAndSet is ideal for 'only one thread should win this transition'); building blocks for custom lock-free data structures and algorithms; ID generators.

compareAndSet(false, true) guarantees that even if many threads call startOnce() concurrently, exactly one of them observes true (the winner) and proceeds with initialization.

class OneTimeInitializer {
    private final AtomicBoolean started = new AtomicBoolean(false);

    void startOnce() {
        if (started.compareAndSet(false, true)) {
            System.out.println("starting -- this thread won the race");
        } else {
            System.out.println("already started by another thread");
        }
    }
}

Key Takeaways

Atomic classes (AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference<V>) provide atomic compound operations on a single variable without requiring explicit locking.

compareAndSet(expected, new) is the foundational hardware-backed primitive most other atomic operations are built from.

Higher-level methods like incrementAndGet() and updateAndGet() are typically implemented as retry loops around compare-and-set.

Atomic classes only guarantee atomicity per individual variable — an invariant spanning multiple atomic fields still needs a lock or a single AtomicReference to an immutable composite object.

Atomicity and lock-freedom are related but distinct properties; most CAS-based atomic operations are lock-free in practice but this is not a language-mandated guarantee.

Atomic classes provide everything volatile provides for individual reads/writes, plus atomic compound (read-modify-write) operations.

AtomicInteger a = new AtomicInteger(); AtomicInteger b = new AtomicInteger(); a.set(computeA()); b.set(computeB()); // NOT atomic as a pair synchronized (lock) { a.set(computeA()); b.set(computeB()); } // or combine into one AtomicReference<Pair>

Each set() call is individually atomic, but nothing guarantees a and b are updated (or observed) together — a lock or a single combined AtomicReference is needed to protect a multi-field invariant.

int oldValue; do { oldValue = counter.get(); } while (!counter.compareAndSet(oldValue, oldValue + 1)); // reinventing incrementAndGet() counter.incrementAndGet(); // already does exactly this retry loop, correctly and efficiently

The atomic classes already implement the compare-and-set retry loop for common operations — hand-rolling it is unnecessary and more error-prone than calling the provided method.

int result = counter.updateAndGet(current -> { logToDatabase(current); // side effect -- may run multiple times under contention! return current + 1; });

int result = counter.incrementAndGet(); logToDatabase(result); // do side effects OUTSIDE the atomic update function

The lambda passed to updateAndGet()/accumulateAndGet() may be invoked more than once if the internal compare-and-set retries under contention, so it must be a pure function; side effects belong outside the call.

Why do atomic classes exist when volatile fields and synchronized already exist?

volatile guarantees visibility for individual reads/writes but not atomicity of compound operations; synchronized/Lock give atomicity for arbitrary critical sections but at the cost of blocking and lock-acquisition overhead. Atomic classes fill the specific gap of atomic compound operations on a single variable, typically without blocking, using hardware-backed compare-and-set.

What does compareAndSet(expected, newValue) do, and what does it return?

It atomically checks whether the current value equals 'expected'; if so, it sets the value to newValue and returns true. If the current value does not match 'expected' (because another thread changed it), it leaves the value unchanged and returns false.

How is a method like incrementAndGet() typically implemented internally?

As a compare-and-set retry loop: read the current value, compute the new value, attempt compareAndSet(old, new); if that fails because another thread updated the value in between, retry from the top with the new current value, repeating until the CAS succeeds.

Why must the lambda passed to updateAndGet() or accumulateAndGet() be free of side effects?

Because the underlying implementation may retry the compare-and-set loop under contention, the supplied function can be invoked more than once for a single logical call; if it has side effects (like logging or mutating external state), those side effects can happen multiple times, producing incorrect or duplicated behavior.

Is an atomic class automatically lock-free? Explain the distinction.

Not automatically or by language guarantee — atomicity (the operation appears indivisible to other threads) and lock-freedom (no thread ever waits for another thread to make progress) are related but separate properties. Most CAS-based atomic operations are lock-free in practice on modern hardware, but the API does not mandate a specific implementation strategy for every JVM.

If a class has two AtomicInteger fields, x and y, that represent coordinates and must always be read/written together consistently, do the atomic classes guarantee that consistency?

No. Each AtomicInteger only guarantees atomicity for operations on itself individually. A thread could observe x updated and y not yet updated (or vice versa). Preserving a joint invariant across multiple fields requires an explicit lock around both updates, or combining them into a single AtomicReference to an immutable composite object (e.g. a record).

What does AtomicInteger provide on top of what a plain volatile int already provides?

A volatile int already gives visible, ordered individual reads and writes. AtomicInteger adds atomic compound (read-modify-write) operations like incrementAndGet(), addAndGet(), and compareAndSet(), so multi-step updates on the value are indivisible with respect to other threads, not just each individual read or write.

Write a thread-safe ID generator using AtomicLong that returns a unique, monotonically increasing id on each call.

import java.util.concurrent.atomic.AtomicLong; class IdGenerator { private final AtomicLong nextId = new AtomicLong(1); long next() { return nextId.getAndIncrement(); // atomic: returns the id to use, then bumps the counter } } getAndIncrement() atomically reads the current value and increments it in one indivisible step, so even if many threads call next() concurrently, every call gets a distinct value and no two threads can ever receive the same id.

What does this print, and why?

Prints '5, 10'. getAndAdd(3) returns the value BEFORE adding (5), leaving the counter at 8. addAndGet(2) adds 2 to 8, leaving the counter at 10, and returns the value AFTER adding (10).

Write a lock-free 'set only if not already set' initializer using AtomicReference, where the first thread to call it wins and later calls are no-ops.

import java.util.concurrent.atomic.AtomicReference; class LazyConfig { private final AtomicReference<String> config = new AtomicReference<>(null); boolean initialize(String value) { return config.compareAndSet(null, value); // true only for the first successful caller } String get() { return config.get(); } } compareAndSet(null, value) succeeds and returns true only for whichever thread first executes it while config is still null; every subsequent call (from any thread) sees a non-null current value that doesn't match the expected 'null', so it returns false and leaves the existing value untouched, giving correct 'first writer wins' semantics with no explicit locking.

AtomicInteger counter = new AtomicInteger(5);
int a = counter.getAndAdd(3);
int b = counter.addAndGet(2);
System.out.println(a + ", " + b);