⚡ Advanced Concurrency · Advanced

Atomic classes & CAS in Java

AtomicInteger, compare-and-set, LongAdder under contention.

🧩 The mysteryWhat if threads never waited for a lock, but simply tried, noticed they'd lost the race, and tried again? That's compare-and-set, the trick behind every lock-free counter.

Atomic classes

AtomicInteger, AtomicLong and friends give lock-free, thread-safe updates. incrementAndGet(), addAndGet() and updateAndGet() each perform the whole read-modify-write as one indivisible step, so no update is lost.

AtomicInteger hits = new AtomicInteger();
hits.incrementAndGet();         // 1
hits.addAndGet(5);              // 6
hits.updateAndGet(x -> x * 2);  // 12

CAS: set it only if unchanged

compareAndSet(expected, new): set to new only if the value still equals expected. It's one hardware instruction that returns false if another thread got there first. 1. A and B read 5. 2. A: CAS(5→6) succeeds. 3. B: CAS(5→6) fails. 4. B re-reads 6 and retries: CAS(6→7) succeeds.

🔮 Predict it

Your turn

What does this print?

AtomicInteger n = new AtomicInteger(10);
System.out.println(n.compareAndSet(10, 20));
System.out.println(n.compareAndSet(10, 30));
System.out.println(n.get());
  1. true false 20
  2. true true 30
  3. false false 10
Show the answer

The first CAS finds 10 and sets 20. The second expects 10 but finds 20, so it fails and changes nothing. The value stays 20.

Two atomic calls are not one

✗ Check-then-act
if (stock.get() > 0) {
    stock.decrementAndGet();
    return true;
}
return false;

get() and decrementAndGet() are each atomic, but the gap between them isn't. Two buyers both see 1 and stock drops to -1.

✓ One atomic update
int old = stock.getAndUpdate(
    s -> s > 0 ? s - 1 : s);
return old > 0;

Check and decrement happen in one atomic step; the old value tells you whether you got the item.

🤔 Think first

Guaranteed total?

Two threads each call n.incrementAndGet() 1,000 times on a shared AtomicInteger, then both are joined. Is the result guaranteed to be 2000?

Think about it, then reveal the answer

Yes. incrementAndGet() is a single atomic read-modify-write, so no increment is lost, and after the joins main sees the final value. Compare with volatile int and ++, which loses updates.

LongAdder for hot counters

64 threads hammering one AtomicLong spend their time retrying failed CAS. LongAdder gives contending threads separate cells, so they rarely collide; sum() adds the cells when you read. Catch: sum() isn't an atomic snapshot while updates are in flight. Fine for metrics, not for invariants.

LongAdder requests = new LongAdder();
requests.increment();     // from any thread
long total = requests.sum();
💼 In the real world

Where you'll see it

Request counters, rate limiters and ID generators are often a single atomic. Metrics libraries use LongAdder-style striped counters for hot paths, and ConcurrentHashMap counts its own size with cells adapted from LongAdder. Interview favorite: "How does AtomicInteger work without a lock?" Answer: CAS in a retry loop.

Key takeaways

  1. incrementAndGet(), addAndGet(), updateAndGet() are atomic
  2. compareAndSet(expected, new) returns false if the value changed
  3. LongAdder is faster under contention; sum() isn't a snapshot
  4. Two separate atomic calls together are NOT atomic
🤯 Did you know?

x86 CPUs do compare-and-set in one instruction, LOCK CMPXCHG. Older ARM chips needed a load-linked/store-conditional loop; ARMv8.1 added a real CAS instruction.

Practice questions

What does this print?

AtomicInteger n = new AtomicInteger(5);
System.out.println(n.compareAndSet(5, 8));
System.out.println(n.compareAndSet(5, 9));
System.out.println(n.get());
  1. true false 8
  2. true true 9
  3. false false 5
  4. true false 9
Check your answer

true false 8. The first CAS sees 5 and sets 8. The second expects 5 but finds 8, so it fails and changes nothing.

What does this print?

AtomicInteger n = new AtomicInteger();
Runnable r = () -> {
    for (int i = 0; i < 1000; i++) {
        n.incrementAndGet();
    }
};
Thread a = new Thread(r), b = new Thread(r);
a.start(); b.start();
a.join(); b.join();
System.out.println(n.get());
  1. 2000
  2. 1000
  3. A different number on each run
  4. 0
Check your answer

2000. incrementAndGet() is a single atomic read-modify-write, so no increment is lost. After both joins, the total is exactly 2000.

Next: a lock you hold in your own hands. ReentrantLock can give up, be interrupted and play fair, but you must always release it in finally.