Back to Atomic Classes & CAS
Q3Question 3 of 4

How does Compare-And-Swap work?

Key Concepts
  • CAS is a CPU hardware instruction (CMPXCHG on x86, LDREX/STREX on ARM) that atomically compares a value and sets it if the comparison succeeds.
  • AtomicInteger.incrementAndGet() uses a CAS spin loop: read old value, compute new, CAS. If CAS fails, retry with the fresh value.
  • CAS is lock-free: no thread is ever blocked. Under low contention, CAS succeeds on the first attempt and is very fast.
  • Under high contention (100+ threads), CAS retry loops waste CPU because most attempts fail when another thread modifies the value first.
  • The ABA problem occurs when a value changes from A to B and back to A; CAS sees A and succeeds even though the value was modified.
  • AtomicStampedReference fixes ABA by CAS-ing on both the value and a version stamp.
  • Fixes for high-contention CAS spinning: LongAdder (striped cells), exponential backoff, or partitioned ranges.

Definition

Compare-And-Swap is a CPU hardware instruction that atomically reads, compares, and writes a value in one uninterruptible operation, forming the foundation of all lock-free Atomic classes in Java.
  • CAS operation – atomically: read the value at a memory address, compare it to an expected value, and if they match, write a new value. If they do not match, the operation fails and the caller retries.
  • Hardware instruction – CMPXCHG on x86, LDREX/STREX on ARM. It is a single CPU instruction that cannot be interrupted by another thread.
  • Lock-free – no thread is ever BLOCKED. If a CAS fails, the thread reads the new value and retries. Progress is always guaranteed globally (at least one thread succeeds per round).
  • Spin loop – AtomicInteger.incrementAndGet() loops: read current, compute new, CAS. If CAS fails (another thread changed the value), repeat with the updated value.
  • High contention – with 100 threads CAS-ing the same variable, most attempts fail on each round. The CPU spends cycles on retries rather than useful work.
  • ABA problem – value changes A to B to A. CAS checks only the value, not whether it was modified. Fix: AtomicStampedReference pairs the value with a monotonically increasing stamp.

CAS is like a ticket machine at National Bank of India with a simple rule: to change the displayed number, you must know what it currently shows. Rajesh reads "42" and tells the machine: "If it still shows 42, change it to 43." The machine checks, sees 42, and changes it. But if Priya already changed it to 43, the machine says "NOPE, it is 43 now" and Rajesh must retry: read 43, try "if 43, change to 44." Under high contention, 100 people all trying to change the number means 99 fail each round, wasting a lot of effort.

Understanding the Concepts

How does Compare-And-Swap work?

CAS is a single CPU instruction that performs three steps atomically: read the current value at a memory address, compare it to an expected value, and if they match, write a new value. If the current value does not match the expected value (meaning another thread changed it), the instruction returns false and the caller must retry.

JAVA
// Pseudocode for CAS hardware instruction:
// boolean CAS(address, expected, newValue):
//     if memory[address] == expected:
//         memory[address] = newValue
//         return true
//     else:
//         return false   // another thread changed it

How does AtomicInteger use CAS?

JAVA
package com.bank.concurrency;

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    private static final AtomicInteger transactionCount = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 100_000; i++) {
                transactionCount.incrementAndGet();
                // Internally:
                // do {
                //     int prev = get();            // volatile read
                //     int next = prev + 1;          // compute
                // } while (!compareAndSet(prev, next));  // CAS
            }
        };
        Thread t1 = new Thread(task, "Counter-1");
        Thread t2 = new Thread(task, "Counter-2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Count: " + transactionCount.get());  // always 200000
    }
}

Why does CAS spinning hurt under high contention?

With 100 threads all CAS-ing the same AtomicLong, each CAS round has only one winner. The other 99 threads read the new value and retry. On the next round, again only one wins. This means each increment takes roughly 50 attempts on average, wasting 99% of CAS operations.

JAVA
// High contention scenario:
// 100 threads → AtomicLong.incrementAndGet()
// Round 1: Thread-1 CAS(99→100) succeeds. Threads 2-100: fail, retry.
// Round 2: Thread-2 CAS(100→101) succeeds. Threads 3-100: fail, retry.
// Each thread: ~50 retries on average = massive CPU waste!

What is the ABA problem?

The ABA problem occurs when a value changes from A to B and then back to A. A CAS operation that read A initially succeeds because the current value is still A, even though it was modified and restored. For simple counters this is harmless, but for pointer-based data structures like lock-free stacks, ABA can cause corruption.

JAVA
package com.bank.concurrency;

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAFixDemo {
    public static void main(String[] args) {
        // AtomicStampedReference: CAS on BOTH value AND stamp
        AtomicStampedReference<String> ref =
                new AtomicStampedReference<>("SAV2001", 0);

        int[] stampHolder = new int[1];
        String current = ref.get(stampHolder);  // value="SAV2001", stamp=0

        // Another thread changes SAV2001 → CUR3001 → SAV2001 (ABA!)
        ref.compareAndSet("SAV2001", "CUR3001", 0, 1);  // stamp: 0→1
        ref.compareAndSet("CUR3001", "SAV2001", 1, 2);  // stamp: 1→2

        // Our CAS: expects stamp=0, but stamp is now 2 → FAILS (detects ABA!)
        boolean success = ref.compareAndSet("SAV2001", "FD4001", 0, 1);
        System.out.println("CAS success: " + success);  // false: stamp mismatch
    }
}

What are the fixes for high-contention CAS spinning?

Three strategies reduce contention:

  • LongAdder – striped cells so each thread CAS-es its own cell with zero contention.
  • Exponential backoff – after a failed CAS, sleep for a random duration that doubles with each retry, reducing the collision rate.
  • Partitioned ranges – assign each thread a range of sequence numbers (Thread-1: 0-999, Thread-2: 1000-1999) so no CAS contention occurs at all.

How is CAS different from synchronized?

| Aspect | CAS (lock-free) | synchronized (blocking) |
|---|---|---|
| Blocking | Never | Yes (thread parks) |
| Context switch | None | Yes (park/unpark) |
| Low contention | Very fast (one CAS) | Fast (biased/thin lock) |
| High contention | CPU spinning (waste) | Thread parks (less CPU waste) |
| Deadlock possible? | No | Yes |
| Progress guarantee | Lock-free (global progress) | Blocking (no guarantee) |

What are common interview follow-up questions?

  • Is CAS wait-free? No, it is lock-free. Lock-free guarantees that at least one thread makes progress in every round. Wait-free would guarantee that every thread makes progress in bounded steps. AtomicInteger.incrementAndGet() is lock-free but not wait-free.
  • What CPU instruction implements CAS? CMPXCHG on x86 (with LOCK prefix for multi-core), LDREX/STREX on ARM, LL/SC on MIPS.

Visual Overview

DIAGRAM
  CAS Spin Loop (AtomicInteger.incrementAndGet)
  ══════════════════════════════════════════════

  Thread-1                     Thread-2
  ────────                     ────────
  read: prev=42                read: prev=42
  compute: next=43             compute: next=43
  CAS(42→43): SUCCESS          CAS(42→43): FAIL (now 43!)
                               read: prev=43
                               compute: next=44
                               CAS(43→44): SUCCESS

  ABA Problem:
  Thread-1: read A             Thread-2: A→B→A
  Thread-1: CAS(A→C): SUCCESS  (value is A, but was modified!)
  Fix: AtomicStampedReference(value, stamp)

Key Takeaway

Compare-And-Swap is a CPU hardware instruction that atomically reads, compares, and writes a value, forming the foundation of all lock-free Atomic classes in Java. AtomicInteger.incrementAndGet() uses a CAS spin loop: read the old value, compute the new value, CAS. If another thread changed the value, CAS fails and the loop retries. This is lock-free: no thread is ever blocked, and at least one thread makes progress per round.

Under low contention (2-8 threads), CAS is extremely fast because most attempts succeed on the first try. Under high contention (100+ threads), most CAS attempts fail, wasting CPU on retries. The fix is to reduce contention: LongAdder (striped cells), exponential backoff, or partitioned ranges. The ABA problem (value changes A to B to A, CAS succeeds when it should not) is fixed with AtomicStampedReference, which CAS-es on both the value and a version stamp.

At National Bank of India, a trade sequence counter using one AtomicLong with 200 threads had 95% CAS failure rate. Partitioning the sequence space (each thread gets its own range) eliminated contention entirely.

Remember one sentence: CAS is a lock-free hardware instruction that retries on failure, fast under low contention but costly under high contention where LongAdder or partitioning is the fix.

---

Share
Use ← → arrow keys to navigate