W3docs

Java Atomic Variables

Lock-free thread-safe operations in Java with java.util.concurrent.atomic classes — counters, references, and compare-and-set.

Java Atomic Variables

volatile makes a single read or single write thread-safe. It can't make counter++ thread-safe — that's three operations. The java.util.concurrent.atomic package fills the gap. Its classes wrap a single value and expose operations like increment-and-get and compare-and-set as single atomic instructions — no lock, no synchronized block, just a CPU primitive (compare-and-swap, or CAS) that the JVM compiles down to.

Atomics are the right tool for a surprisingly large number of multithreaded patterns: counters, sequence numbers, flag-like state, and any "publish a new immutable snapshot" idiom. They're faster than synchronized under contention and dramatically simpler than hand-rolling a lock around a single field.

The family

The package has eight commonly used classes:

ClassWrapsCommon operations
AtomicIntegerintget, set, incrementAndGet, addAndGet, compareAndSet
AtomicLonglongsame as above, on long
AtomicBooleanbooleanget, set, compareAndSet
AtomicReference<V>V (any object reference)get, set, compareAndSet, updateAndGet
AtomicIntegerArrayint[]per-index atomic ops
AtomicLongArraylong[]per-index atomic ops
AtomicReferenceArray<V>V[]per-index atomic ops
LongAdder / LongAccumulatorlonghigh-contention counter

The first four are what you'll reach for 99% of the time.

AtomicInteger — the right counter

The replacement for "volatile int plus ++":

AtomicInteger counter = new AtomicInteger();          // starts at 0

counter.incrementAndGet();                            // ++counter, atomic
counter.getAndIncrement();                            // counter++, atomic
counter.addAndGet(5);                                 // counter += 5, atomic
counter.set(42);                                      // counter = 42, atomic
int n = counter.get();                                // read

counter.compareAndSet(42, 100);                       // if (counter == 42) counter = 100; return whether it changed

incrementAndGet is what you want for a simple counter; under the hood it's a CAS loop that the CPU does in one instruction on modern x86 (LOCK XADD). The whole operation is a single bus-level memory transaction — much cheaper than acquiring even an uncontended synchronized lock.

compareAndSet(expected, new) is the building block for almost everything else. It atomically writes new only if the current value is expected, and returns whether the write happened. With it you can build any single-field atomic update:

AtomicInteger max = new AtomicInteger(Integer.MIN_VALUE);
void recordMax(int v) {
  int cur;
  do {
    cur = max.get();
    if (v <= cur) return;                             // nothing to do
  } while (!max.compareAndSet(cur, v));               // retry if someone else updated
}

The CAS loop is the standard pattern: read, compute, try-to-write, retry on conflict. It's how incrementAndGet is implemented; it's how you'd write any compound update on a single field.

Java 8 simplified the loop:

max.updateAndGet(cur -> Math.max(cur, v));            // CAS loop hidden

updateAndGet, accumulateAndGet, and getAndUpdate take a function and do the CAS loop for you. Prefer them when they fit.

AtomicReference<V> — the right way to swap an object

When the shared state is more than a primitive — a configuration map, a cached snapshot, an immutable holder — AtomicReference lets you atomically swap the whole object:

AtomicReference<Config> currentConfig = new AtomicReference<>(initialConfig);

void reload() {
  Config c = readConfigFromDisk();                    // expensive, lock-free
  currentConfig.set(c);                               // publish atomically
}

Config get() { return currentConfig.get(); }

The trick: the contents of Config must be immutable (or not touched after publish). The atomic swap publishes a finished value; if other threads then mutate the value's internals you've lost the safety. This is the immutable snapshot pattern, and it's the way most concurrent caches, route tables, and "global config" objects are built.

updateAndGet on a reference is also extremely useful:

AtomicReference<List<String>> log = new AtomicReference<>(List.of());

void append(String line) {
  log.updateAndGet(old -> {
    var copy = new ArrayList<>(old);
    copy.add(line);
    return List.copyOf(copy);                         // immutable snapshot
  });
}

Every reader gets a consistent immutable list. Writers race; the CAS loop retries the few that lose the race. Cheap under low contention, slow but correct under high.

LongAdder — the high-contention counter

Under heavy contention, AtomicLong.incrementAndGet becomes a bottleneck — every thread is hammering the same memory address, and the CPU has to serialise the bus transactions. LongAdder solves this by maintaining several internal counters, one per CPU, and summing them on read:

LongAdder requestCount = new LongAdder();

void onRequest() { requestCount.increment(); }        // append-only, no contention

long snapshot() { return requestCount.sum(); }        // sums every cell — not atomic but eventually consistent

Use LongAdder when:

  • The counter is incremented from many threads concurrently (think: per-request metrics in a web server).
  • You read it rarely (every few seconds for a dashboard).

Use AtomicLong when:

  • Incrementing is rare or single-threaded.
  • You need an instantaneous accurate read.

LongAdder is one of the fastest concurrent counters anywhere — but the trade is that sum() is not atomic with concurrent increments. For the typical metrics-reporting case, that's fine.

What atomics are not

Atomics scale to one field. They don't compose across multiple fields:

AtomicInteger a = new AtomicInteger();
AtomicInteger b = new AtomicInteger();

a.incrementAndGet();                                  // atomic on its own
b.incrementAndGet();                                  // atomic on its own
// but the pair is NOT atomic — another thread can see new a, old b

If your invariant spans multiple fields ("a == b + 1 always"), you need a lock (or a single atomic on a holder object containing both).

Atomics also don't help with visibility of unrelated fields. Writing to an atomic doesn't publish other fields the way volatile does. Make those other fields volatile (or final, or write them through the atomic).

compareAndExchange and the new API (Java 9+)

Java 9 added compareAndExchange (returns the current value, not just a boolean):

int prev = counter.compareAndExchange(expected, newVal);
if (prev == expected) {                               // we won
  ...
} else {                                              // somebody else got there first
  // prev is the actual current value
}

Java 9 also added the VarHandle API which exposes weak CAS, ordered access, etc., for low-level concurrent libraries. You very rarely need it; mention it here so you've seen the name.

A worked example: counter and snapshot

The program below contrasts four counters: unsynchronised, volatile, AtomicInteger, and LongAdder. All four are hit by 8 threads doing 100,000 increments each.

java— editable, runs on the server

What to take from the run:

  • plain and volatile both lost updates — sometimes spectacularly (a final count far below the expected 800,000). volatile fixes the visibility problem but n++ is still three operations. This is the most important thing to remember about volatile: it does not make compound updates atomic.
  • AtomicInteger produced the exact expected count, every run. The per-increment cost was a few nanoseconds — significantly higher than n++ on a plain int (which is one or two), but no lock acquisitions and no thread blocking. Under contention it's faster than synchronized by a wide margin.
  • LongAdder was the fastest counter under the 8-thread load — it scatters writes across separate cells per CPU so the threads don't contend on a single cache line. The trade is sum() is not atomic with increment() (a reader can see a slightly stale total), which is exactly the right deal for metrics and counters where instantaneous accuracy doesn't matter.
  • The CAS-loop max recorded the largest value seen across all samples. The loop is the general pattern: read the current value, compute the desired new value, try to write it; if somebody else wrote first, the CAS fails and you retry. Most updateAndGet and accumulateAndGet calls are this loop with the boilerplate hidden.
  • The AtomicReference<List<String>> produced an immutable snapshot of the log. Each writer built a new immutable copy and tried to publish it; under contention, two writers might both build a copy and one's CAS fails — that thread retries, reads the freshly-updated list, and merges. The pattern is wasteful under heavy contention (lots of throw-away copies) but ideal for "read-heavy, occasionally-rebuilt" snapshots.

What's next

The next chapter, Java Locks, starts the java.util.concurrent.locks story — the Lock interface, why it exists alongside synchronized, and the capabilities (tryLock, lockInterruptibly, Condition) it adds that the intrinsic monitor doesn't have.

Practice

Practice

Which of these is the correct way to safely increment a shared counter from many threads in a tight loop?