Atomics & Lock-Free Thinking
Compare-and-swap, the atomic classes, and how lock-free algorithms avoid the cost and deadlock risk of locking.
On this page
Locks are the obvious way to protect shared state, but they have costs: contention, context switches, and the ever-present risk of deadlock. For many problems there's a faster, deadlock-free alternative built on a single hardware instruction: compare-and-swap. Understanding it, and the atomic classes built on it, is core senior-level concurrency knowledge.
The problem with count++
Recall from the memory-model lesson that count++ is three operations - read, add, write - so two threads
can interleave and lose updates. synchronized fixes it but serializes access. The atomic classes
fix it without a lock:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic read-modify-write, no lock, no lost updatesCompare-and-swap (CAS)
Atomics are built on compare-and-swap, a CPU instruction that does "if this memory location still holds the value I expect, set it to the new value - atomically." If another thread changed it in the meantime, the CAS fails and you retry:
// what incrementAndGet does under the hood, conceptually:
int current, next;
do {
current = value.get(); // read the current value
next = current + 1; // compute the new value
} while (!value.compareAndSet(current, next)); // retry if someone else changed itThis is lock-free: no thread ever blocks another. Under low-to-moderate contention it's much faster than locking (no OS-level parking/waking), and it cannot deadlock because there's no lock to hold. The trade-off is that under very high contention many CAS retries can waste CPU (a "livelock"-ish spin), where a lock might be better.
The atomic toolkit
Java's java.util.concurrent.atomic package offers AtomicInteger, AtomicLong, AtomicReference
(CAS on an object reference - useful for lock-free stacks and updating immutable snapshots), and
LongAdder (a high-throughput counter that shards updates across cells to reduce contention). Reach for
these before writing a synchronized block around a single variable.
The ABA problem
CAS checks that a value is unchanged, but a value could change from A to B and back to A between your read and your CAS - which the CAS can't detect, potentially corrupting lock-free data structures. This is the ABA problem. AtomicStampedReference addresses it by pairing the value with a version stamp that changes on every update. It rarely bites simple counters, but it's the kind of subtlety senior interviews probe.
Locking is like taking exclusive edit access to a document - nobody else can touch it until you release, and if two people both wait for each other's documents, everyone's stuck (deadlock). CAS is the optimistic approach: you read the current version, make your change, and hit 'save only if the document is still at the version I read.' If someone else saved first, your save is rejected and you simply re-read and retry. Nobody is ever locked out, so no deadlock - but if dozens of people hammer the same paragraph, you'll retry a lot, and occasionally a plain lock is calmer.
You need a counter incremented by many threads. Compare using an AtomicInteger against a synchronized block around an int, in terms of correctness, performance, and deadlock risk.
What does compare-and-swap (CAS) do, and why is it valuable?
Key takeaways
- Atomic classes (AtomicInteger, AtomicReference, LongAdder) provide thread-safe updates without locks.
- They're built on compare-and-swap: atomically set a value only if it still equals the expected one, else retry.
- Lock-free updates never block another thread and cannot deadlock - faster than locking under low/moderate contention.
- Under very high contention, CAS retries can waste CPU; LongAdder shards counts, and sometimes a lock is calmer.
- The ABA problem (value changes A->B->A undetected) can corrupt lock-free structures; AtomicStampedReference adds a version stamp.