skip to content

How do CAS-based atomics compare to a synchronized counter, and when would you choose one over the other?

level: seniorimportance: should knowfreq 62%

answer

  1. synchronized = pessimistic/blocking; atomics = optimistic/lock-free
  2. Single cell → atomic; multi-field invariant → lock
  3. Atomics: no deadlock, no parking, spin under contention
  4. Bundle fields into AtomicReference<immutable> to CAS together
  5. Hot counter → LongAdder beats both

basics

~20 s

Atomics use a lock-free CAS retry loop; synchronized takes a real lock. Atomics are usually faster and deadlock-free for single-variable updates, but synchronized is simpler and necessary when you must update several things together as one atomic block.

solid answer

~50 s

A `synchronized` counter is *pessimistic and blocking*: a thread acquires the object's monitor, runs the critical section, releases it; contending threads wait (BLOCKED). An `AtomicInteger` is *optimistic and non-blocking*: it reads, computes, and `compareAndSet`s in a retry loop with no lock. For a single shared variable, atomics typically win — no lock overhead, no deadlock, no thread parking, and better scalability under low-to-moderate contention. Choose `synchronized` (or an explicit `Lock`) when the atomic unit spans **multiple** variables or steps that must not interleave — CAS only makes one cell atomic, so a compound invariant across two fields needs a lock (or an `AtomicReference` to an immutable holder bundling them). Under *very high* contention a single CAS cell becomes a hotspot that spins; there `LongAdder` (striped cells) outperforms both. So: single var → atomic; multi-step invariant → lock; extreme contention on a counter → LongAdder.

code

java · 10 lines
java
// CAS two fields together via an immutable bundle held in one AtomicReference.
record Range(int low, int high) {}

AtomicReference<Range> ref = new AtomicReference<>(new Range(0, 10));

Range cur, next;
do {
    cur = ref.get();
    next = new Range(cur.low() + 1, cur.high() + 1); // invariant low<=high preserved together
} while (!ref.compareAndSet(cur, next)); // single-cell CAS swaps both fields atomically

go deeper

for a junior

Knows both AtomicInteger and synchronized make a counter thread-safe, and can use either. Not expected to compare performance or contention behavior deeply.

for a middle

Explains that synchronized blocks while atomics retry, that atomics avoid locks, and gives the basic rule: single variable use atomic, multiple steps use a lock.

for a senior

Articulates optimistic vs pessimistic trade-offs, the single-cell limit of CAS and the AtomicReference-to-immutable-bundle workaround, contention behavior (spinning), and when LongAdder is the right tool; treats it as a measured engineering choice.

for a principal

Reasons about contention profiles, cache-line and false-sharing effects, latency tails of blocking vs lock-free, deadlock-freedom and progress guarantees, and the maintainability cost of lock-free designs; sets team guidance on when to reach for each.

## Two strategies for shared mutable state When multiple threads update shared data, you need to prevent interleavings that corrupt it. There are two broad strategies. **Pessimistic / blocking (`synchronized`, `ReentrantLock`).** Assume a conflict will happen, so *exclude* other threads up front by taking a **lock**. Only the lock holder runs the **critical section** (the protected code). Others wait — in Java they go to the `BLOCKED` state, parked by the OS until the lock frees. Correct and simple, but: lock acquisition has overhead, a descheduled lock-holder blocks everyone, and multiple locks can **deadlock**. **Optimistic / non-blocking (CAS atomics).** Assume *no* conflict, do the work, then *check* with `compareAndSet`: if the value is still what you read, commit; otherwise redo. No lock, no parking, no deadlock. The cost is **spinning** — repeatedly retrying when conflicts are frequent. ## Side-by-side: incrementing a counter ```java // Pessimistic: blocking lock class LockedCounter { private long count = 0; synchronized void inc() { count++; } // monitor held for the RMW synchronized long get() { return count; } } // Optimistic: lock-free CAS class AtomicCounter { private final AtomicLong count = new AtomicLong(); void inc() { count.incrementAndGet(); } // CAS retry loop, no lock long get() { return count.get(); } } ``` Both are correct. `AtomicCounter` avoids the monitor entirely: a thread that loses the CAS race just retries, it never blocks another thread. ## Where atomics win - **No blocking / no deadlock.** A lock-free update can't deadlock and can't be stalled by a descheduled lock holder. Good for latency-sensitive paths and for code that must stay responsive. - **Lower overhead under low/moderate contention.** No monitor enter/exit, no thread parking/unparking. - **Scalability** on a single hot variable up to a point. ## Where `synchronized`/`Lock` wins (and atomics can't help) - **Compound invariants across multiple fields.** CAS is atomic for **one** cell only. If you must keep `low <= high` while updating both, two separate atomics can interleave and break the invariant. You need a lock around the whole update — or bundle both fields into one immutable object held by a single `AtomicReference` and CAS the whole reference. - **Multi-step critical sections** (read several things, decide, write several things) — a lock is the natural fit. - **Condition waiting** (wait/notify, `Condition`) — atomics don't provide blocking coordination. - **Simplicity.** A short `synchronized` block is easy to read and reason about; hand-rolled lock-free code is notoriously subtle. ## Where *both* lose: extreme contention Under a storm of writers to one counter, the single CAS cell is a cache-line hotspot and CAS attempts keep failing and retrying (and `synchronized` serializes everyone). **`LongAdder`** solves this by striping the count across multiple internal cells so threads rarely collide; `sum()` adds them up. It trades an exact, instantaneously-consistent value for throughput — ideal for metrics/counters that are written constantly and read occasionally. ## Decision guide | Situation | Pick | |---|---| | One shared variable, simple update | Atomic (CAS) | | Invariant spanning multiple fields / multi-step section | `synchronized` / `Lock` (or `AtomicReference` to an immutable bundle) | | Need to block/wait for a condition | `Lock` + `Condition`, or `synchronized` + wait/notify | | Very hot counter, many writers | `LongAdder` / `LongAccumulator` | ## Memory-model note Both approaches give the right visibility: releasing a monitor and reading/writing an atomic both establish *happens-before* edges, so updates made by one thread are visible to the next. Choosing atomics over a lock is a performance/structure decision, not a correctness shortcut around the memory model.

  • You need to atomically update two fields together with CAS. How?
    You can't CAS two separate cells atomically. Put both fields into a single immutable holder object and hold it in one AtomicReference; then compute a new holder and compareAndSet the whole reference. If the swap fails, recompute from the fresh holder and retry. This makes the multi-field update a single-cell CAS.
  • Is replacing synchronized with AtomicInteger guaranteed to be faster?
    No. Under low/moderate contention atomics usually win (no lock overhead, no blocking), but under heavy contention the single CAS cell becomes a hotspot with many failed retries, and a striped LongAdder or even a coarse lock can do better. Always measure; the right choice depends on the contention profile and whether the update is single-cell.

saying these in an interview costs you the question

  • Claiming atomics are always faster than synchronized regardless of contention (they spin and lose under heavy contention)
  • Using two separate atomics to maintain an invariant between two fields (they can interleave and break it)
  • Thinking you can wait/block on a condition with an atomic — you can't, that needs a lock/monitor
  • Assuming switching to atomics fixes a missing-visibility bug that was really a multi-step race
  • Reaching for hand-rolled lock-free code when a short synchronized block would be clearer and correct

context