Atomic classes & CAS in Java
AtomicInteger, compare-and-set, LongAdder under contention.
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); // 12CAS: 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.
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());true false 20true true 30false 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
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.
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.
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();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
- incrementAndGet(), addAndGet(), updateAndGet() are atomic
- compareAndSet(expected, new) returns false if the value changed
- LongAdder is faster under contention; sum() isn't a snapshot
- Two separate atomic calls together are NOT atomic
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());- true false 8
- true true 9
- false false 5
- 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());- 2000
- 1000
- A different number on each run
- 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.