Atomic Variables and Compare-and-Set (CAS) in Java

Java atomic variables provide thread-safe operations without requiring an explicit lock, using atomic read-modify-write primitives such as compare-and-set (CAS). CAS updates a value only when it still equals an expected value, while also providing the memory-ordering guarantees defined by the atomic class.

Classes such as `AtomicInteger`, `AtomicLong`, and `AtomicReference` support atomic operations on shared state. They are useful when a thread needs to update a value safely while avoiding a traditional `synchronized` block or explicit `Lock`.

The core CAS operation takes an expected value and a new value. Conceptually, it performs one indivisible operation: if the current value equals the expected value, replace it with the new value and report success; otherwise leave it unchanged and report failure. This prevents a thread from overwriting a change made by another thread between its read and write.

A common pattern is a CAS retry loop. A thread reads the current value, calculates a new value, and calls `compareAndSet`. If another thread changed the value first, CAS fails, so the thread rereads the value and tries again. This is often called a lock-free update pattern, although an individual algorithm must be analyzed separately to establish its precise progress guarantee.

Atomic variables also have memory-visibility effects. Successful atomic updates and the corresponding atomic reads participate in the Java Memory Model's ordering rules, allowing threads to coordinate through the atomic variable rather than relying on an ordinary unsynchronized field. Atomicity therefore addresses both the indivisibility of the update and the required memory-ordering semantics.

Key points

Interview questions

What is compare-and-set (CAS), and how does it work?

CAS compares the current value with an expected value and changes it to a supplied new value only when they match. The operation is atomic, so another thread cannot interleave a conflicting update between the comparison and replacement. It returns whether the update succeeded. A failed CAS indicates that the observed value is no longer current.

Why is `volatile int counter` not sufficient for `counter++`?

A volatile field gives threads visibility of reads and writes, but `counter++` consists of a read followed by an addition and a write. Two threads can read the same old value and then both write the same incremented value, losing one update. An `AtomicInteger` can perform the increment as one atomic operation. CAS can also implement the read-modify-write sequence by retrying when another thread changes the value.

How would you implement an atomic increment using CAS?

Read the current value, calculate the desired value, and call `compareAndSet` with the old value and the calculated value. If CAS succeeds, the increment is complete. If it fails, another thread changed the value, so the loop reads the new value and retries. The important property is that the calculation is based on the value that CAS verifies before committing the update.

What memory-model guarantees do Java atomic variables provide?

Atomic classes are designed to provide atomic operations together with the memory effects specified by the Java Memory Model. In particular, atomic reads and updates provide synchronization and visibility semantics that allow threads to coordinate through the atomic variable. This is why an atomic variable is more than merely a CPU-level indivisible update. The exact ordering guarantees should be understood from the API contract rather than inferred solely from the hardware instruction.

What is the ABA problem in CAS?

ABA occurs when a thread reads value A, another thread changes it to B and later changes it back to A, and the first thread's CAS sees A and therefore succeeds. The CAS comparison cannot tell that an intermediate change occurred when it compares only the value. This can be problematic when the intermediate transition matters to correctness, especially in lock-free data structures. Versioning or stamped references can be used when the algorithm needs to detect such changes.