Compare and swap (CAS) is an atomic operation that writes a new value to a variable only if the variable still holds the value we expect, and it reports whether the write happened. In Java, the method compareAndSet(expected, newValue) of the classes in java.util.concurrent.atomic runs CAS, and the CPU does the check and the write as one step that no other thread can interrupt.
We use CAS to update shared data from many threads without a lock. The atomic classes, such as AtomicInteger and AtomicReference, build their updates on CAS, and so do lock-free collections such as ConcurrentLinkedQueue.
The following example books seats on a bus trip with compareAndSet() and compareAndExchange(), with the result of each line as a comment.
AtomicInteger seatsLeft = new AtomicInteger(10);
boolean first = seatsLeft.compareAndSet(10, 9); // true, seatsLeft = 9
boolean second = seatsLeft.compareAndSet(10, 9); // false, seatsLeft = 9 (expected 10, found 9)
int witness = seatsLeft.compareAndExchange(9, 8); // witness = 9, seatsLeft = 8
int stale = seatsLeft.compareAndExchange(9, 7); // stale = 8, seatsLeft = 8
Notice that the second call fails because the value is no longer 10, and a failed CAS leaves the value unchanged. The method compareAndExchange() returns the value it found, so the caller learns the current value from a failed call.
Next, we trace two threads through a CAS step by step and write a retry loop around compareAndSet(). After that, we cover the == comparison trap, CAS on a plain field with VarHandle, the ABA problem and the cases where a lock is the better choice.
1. What Is Compare and Swap?
A CAS operation takes three operands.
- A memory location V, which holds the shared value.
- The expected value A, which the thread read from V earlier.
- The new value B, which the thread wants to write.
If V still holds A, CAS writes B to V. If V holds anything else, another thread changed the value after our read, so CAS writes nothing and reports the failure. Depending on the method, the report is either a boolean (compareAndSet()) or the value that CAS found in V (compareAndExchange()).
The following example writes the CAS rule as a Java method. We need a synchronized method here only to make the check and the write one step, whereas the CPU does the same without a lock.
public synchronized int compareAndSwap(int expected, int newValue) {
int current = value;
if (current == expected) {
value = newValue;
}
return current; // the caller compares it with expected
}
The real CAS is one CPU instruction, so no thread can run between the check and the write. On x86 CPUs, the JVM uses the CMPXCHG instruction with a LOCK prefix, and on 64-bit ARM CPUs it uses either a CAS instruction or a load-exclusive and store-exclusive pair. Java code reaches these instructions through the atomic classes and VarHandle, so we never write them ourselves.
1.1. Pessimistic vs Optimistic Locking
A synchronized block or a Lock is a pessimistic technique. The thread takes the lock first, because it assumes that another thread will interfere, and every other thread that wants the same lock waits until it is free.
CAS is an optimistic technique. The thread computes the new value without any lock and checks for interference only at the moment of the write. When the check fails, the thread reads the value again and retries, so a slow thread never makes the other threads wait.
| Pessimistic (synchronized, Lock) | Optimistic (CAS) | |
|---|---|---|
| When it checks for other threads | before the update, by taking the lock | at the write, by comparing the value |
| What a losing thread does | waits, and the OS may park the thread | reads the new value and retries |
| Scope | any block of code, many fields | one variable per CAS |
| Deadlock possible | yes, with two or more locks | no |
| Cost under low contention | lock and unlock on every update | one CAS instruction |
| Cost under high contention | threads wait in a queue | many failed CAS calls and retries |
For example, a bus booking app sells the last seats of a popular trip, and hundreds of requests arrive in the same second. With a lock, each request thread waits for its turn. With CAS, each thread tries to take a seat, and a thread that loses reads the new seat count and tries again or stops when the trip is sold out.
2. Tracing Two Threads Through a CAS
The memory location V holds 10, and two threads want to add 1 to it. The steps match the numbers in the diagram.
- Both threads read 10 and compute 11 before either one writes.
- Thread 1 calls CAS with A = 10 and B = 11. The value V is 10, so CAS writes 11 and succeeds.
- Thread 2 calls CAS with A = 10 and B = 11. The value V is already 11, so CAS writes nothing and returns 11.
- Thread 2 retries with the value it got back, so A = 11 and B = 12. This time V equals A, so CAS writes 12.

We can see that the losing thread is not suspended. It gets the current value back and decides what to do next, either retry or give up. The emulated method from section 1 prints the same values for the three calls.
EmulatedCas v = new EmulatedCas(10);
int t1 = v.compareAndSwap(10, 11); // t1 = 10, value = 11 (success)
int t2 = v.compareAndSwap(10, 11); // t2 = 11, value = 11 (failure)
int retry = v.compareAndSwap(11, 12); // retry = 11, value = 12 (success)
3. Writing a CAS Loop
A single CAS either succeeds or fails, so real code puts it in a loop. The loop reads the current value, computes the new value from it, and calls compareAndSet(). When the call fails, the loop starts over with a fresh read.
One CAS updates one variable. When two values must change together, we keep them in an immutable object and update an AtomicReference to that object. Every update creates a new object, so no thread ever sees one value changed and the other one not.
The following example is the booking service of a bus trip with 100,000 seats. The record SeatStats holds the seats left and the tickets sold, and book() returns a new SeatStats with one seat less and one ticket more. The examples run on Java 25, and the code compiles on Java 21 or later.
record SeatStats(int seatsLeft, int ticketsSold) {
SeatStats book() {
return new SeatStats(seatsLeft - 1, ticketsSold + 1);
}
}
AtomicReference<SeatStats> stats = new AtomicReference<>(new SeatStats(100_000, 0));
boolean bookSeat() {
while (true) {
SeatStats current = stats.get(); // 1. read the current snapshot
if (current.seatsLeft() == 0) {
return false; // sold out, no write
}
SeatStats next = current.book(); // 2. compute a new snapshot
if (stats.compareAndSet(current, next)) { // 3. write only if nobody changed it
return true;
}
retries.increment(); // another thread won, so try again
}
}
Four threads sent 50,000 booking requests each, so 200,000 requests competed for 100,000 seats. The loop also counted the failed compareAndSet() calls.
CAS loop: booked = 100000, rejected = 100000, stats = SeatStats[seatsLeft=0, ticketsSold=100000], failed CAS retries = 360
getAndUpdate: booked = 100000, stats = SeatStats[seatsLeft=0, ticketsSold=100000]
The number of retries changed on every run, from 2 to 360 in our runs on a 2-CPU machine, but the trip never sold more than 100,000 seats. The second line comes from the same rule written with getAndUpdate(), which runs the CAS loop for us.
SeatStats before = stats.getAndUpdate(s -> s.seatsLeft() > 0 ? s.book() : s);
boolean booked = before.seatsLeft() > 0;
The function passed to getAndUpdate() or updateAndGet() must have no side effects, because the method calls it again after every failed CAS. A function that sends a booking email would send it twice when one CAS fails. For the full list of update methods on a number, read the AtomicInteger tutorial.
4. CAS Compares References With ==
The method AtomicReference.compareAndSet() compares the current value with the expected value using ==, not equals(). So the expected value must be the same object that the reference holds, not an equal copy of it.
The trap shows up with boxed numbers. By default, Java caches Integer objects only from -128 to 127, so two autoboxed 1000 values are two different objects, and the CAS fails although the values are equal.
AtomicReference<Integer> price = new AtomicReference<>(1000);
boolean changed = price.compareAndSet(1000, 1200); // false (two different Integer objects)
Integer current = price.get();
boolean fixed = price.compareAndSet(current, 1200); // true, price = 1200
The safe pattern is the one from section 3. We pass the object that we read with get() as the expected value, never a new object. For numbers, we use AtomicInteger or AtomicLong, which compare primitive values. The same Integer cache is the reason why == on Integer objects works for small values and fails for large ones.
5. CAS on a Field With VarHandle
An AtomicInteger is a separate object, so every booking object with an AtomicInteger field carries one more object on the heap. When an app keeps millions of such objects in memory, we can run CAS on a plain volatile int field instead. A VarHandle, added in Java 9 by JEP 193, gives us the CAS methods for one field of a class.
The following example keeps the free seats of a bus trip in a volatile int field. The static VarHandle points to the field, and compareAndSet() takes the object as its first argument.
class BusTrip {
private static final VarHandle SEATS_LEFT;
static {
try {
SEATS_LEFT = MethodHandles.lookup().findVarHandle(BusTrip.class, "seatsLeft", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
private volatile int seatsLeft;
BusTrip(int seats) {
this.seatsLeft = seats;
}
boolean bookSeat() {
int current;
do {
current = seatsLeft;
if (current == 0) {
return false;
}
} while (!SEATS_LEFT.compareAndSet(this, current, current - 1));
return true;
}
}
BusTrip trip = new BusTrip(2);
boolean a = trip.bookSeat(); // true, seatsLeft = 1
boolean b = trip.bookSeat(); // true, seatsLeft = 0
boolean c = trip.bookSeat(); // false, seatsLeft = 0
JDK classes such as ConcurrentLinkedQueue and FutureTask use the same technique, and VarHandle is the supported replacement for the internal Unsafe class that older articles show. In application code, we choose an AtomicInteger first, because it is simpler to read, and switch to a VarHandle only when the extra objects show up in a heap profile.
6. The ABA Problem
CAS checks only the value, not the history of the value. If another thread changes the value from A to B and back to A between our read and our CAS, the CAS succeeds, because the value looks unchanged. The situation is called the ABA problem.
For example, a ticket desk thread reads seat 12 as “free” and starts to assign the seat to Raj. Meanwhile, Anna books seat 12 and cancels the booking, so the seat is “free” again. The desk thread runs its CAS from “free” to “raj”, and the CAS succeeds, although the seat went through a booking that the desk thread never saw.

For a seat, the ABA case is often harmless. It breaks code when the value is a reference whose meaning changed, for example the head node of a lock-free stack that was removed and pushed again, so the next pointer we read earlier is no longer valid.
The class AtomicStampedReference fixes the ABA problem. It pairs the reference with an int stamp, and its compareAndSet() succeeds only when both the reference and the stamp match. We increment the stamp on every write, so A at stamp 0 is not the same as A at stamp 2.
String free = "free";
String anna = "anna";
String raj = "raj";
AtomicReference<String> seat = new AtomicReference<>(free);
String seen = seat.get(); // seen = "free"
seat.set(anna); // another thread books the seat
seat.set(free); // and cancels it
boolean plain = seat.compareAndSet(seen, raj); // true, the change is not detected
AtomicStampedReference<String> stamped = new AtomicStampedReference<>(free, 0);
int[] stampHolder = new int[1];
String seenRef = stamped.get(stampHolder); // seenRef = "free", stampHolder[0] = 0
int seenStamp = stampHolder[0];
stamped.compareAndSet(free, anna, 0, 1); // another thread books, stamp = 1
stamped.compareAndSet(anna, free, 1, 2); // and cancels, stamp = 2
boolean safe = stamped.compareAndSet(seenRef, raj, seenStamp, seenStamp + 1); // false, stamp 0 != 2
The method get(int[]) returns the reference and puts the stamp into the array, so we read both values in one atomic step. When we need only a flag instead of a counter, AtomicMarkableReference pairs the reference with a boolean.
7. When CAS Is Slower Than a Lock
CAS is fast when threads rarely update the same variable at the same moment. Under heavy contention (many threads writing one variable), most CAS calls fail, and every failed call costs a fresh read and a new attempt. The threads keep the CPU busy without making progress, whereas a lock would park the waiting threads.
- When many threads increment one counter, we use LongAdder, which spreads the updates over several internal cells.
- When the update needs two or more objects that cannot share one immutable snapshot, such as two bank accounts, we use a lock.
- When the update function is slow, for example it calls a database, a failed CAS repeats the slow work, so we use a lock.
- When the update is a short computation on one variable, CAS is the right choice.
A CAS loop guarantees that some thread always makes progress, but it does not guarantee that a given thread ever wins. In practice, a thread that loses many times in a row is rare, and the JDK classes accept that risk.
8. Compare and Swap FAQs
8.1. Is CAS Faster Than synchronized?
Yes, under low and moderate contention. A successful CAS is one instruction, whereas a contended lock can park a thread and wake it up later through the operating system. Under very high contention on one variable, the retries make CAS slower, as section 7 explains, so we measure with our own load before switching.
8.2. What Is the Difference Between compareAndSet() and weakCompareAndSet()?
The method compareAndSet() fails only when the value differs from the expected value. A weak CAS such as weakCompareAndSetVolatile() may also fail spuriously, i.e. return false although the value matched, which lets the JVM use cheaper instructions on some CPUs. We use a weak CAS only inside a retry loop, where a spurious failure costs one more iteration. On AtomicInteger, the old weakCompareAndSet() is deprecated since Java 9, and weakCompareAndSetPlain() replaces it.
8.3. Does ConcurrentHashMap Use CAS?
Yes. When put() adds the first entry to an empty bin of the table, ConcurrentHashMap inserts the node with a CAS and takes no lock. When the bin already has entries, it locks only that bin. The ConcurrentHashMap best practices cover the methods that use these atomic updates, such as compute() and merge().
8.4. Does a Failed CAS Block the Thread?
No. A failed CAS returns at once, either with false or with the value it found. The thread decides what to do next, for example retry or return an error to the user. Code built on CAS is called non-blocking, and the ConcurrentLinkedDeque is one example of a non-blocking collection.
9. Conclusion
Compare and swap writes a new value only when the variable still holds the value we read, and the CPU runs the check and the write as one instruction. In Java, we call it through compareAndSet() or compareAndExchange() on the atomic classes, or through a VarHandle on a volatile field.
A CAS loop retries with a fresh read until the write succeeds, so threads never wait for a lock. To change several values together, we put them in an immutable record and swap the reference. We keep the update functions free of side effects, and we pass the object we read as the expected value, because AtomicReference compares with ==.
CAS has two limits to keep in mind. It cannot see an A-B-A change, which AtomicStampedReference fixes with a stamp, and under heavy contention on one variable, LongAdder or a lock does better.
10. References
- AtomicInteger Javadoc (Java 25)
- AtomicReference Javadoc (Java 25)
- AtomicStampedReference Javadoc (Java 25)
- VarHandle Javadoc (Java 25)
- java.util.concurrent.atomic package summary (Java 25)
- JEP 193: Variable Handles
Happy Learning !!
Thanks for the detailed post.
Is there a specific exception that is raised when a competing thread fails to update an atomic variable, since the case needs to be handled at code level for a retry. If not, how does the competing thread updates an atomic variable on first failure?
How can we prove java.util.ArrayList is not thread safe with programmatically?
Suppose Thread1 and Thread2 both comes and found A = V true.
Then both the threads will try to set new value.
So resulting output will be 11 instead of 12 Right ?
No. As I said in first para itself, it’s atomic operation. If thread A comes first then it will complete its operation; only then B can try.
So, if thread B is waiting for thread A to complete its operation how it is good then using synchronized keyword ?
Please read again section “Compare and Swap Algorithm”. Its an atomic operation. Advantage is that there is no wait-notify logic in application code, so no overhead. This atomicity is maintained at OS layer.
I got that in first sentence it is atomic it will be done successfully or it will fail.
My concern was if it is not synchronized or lock is not taken it may result that both threads read same value and both threads go for update. Basic Semaphore problem.
Also, I didn’t understand if its atomic why compare , set it directly.
Never mind Thanks
Good Explanation —
I want to know java already has volatile variables which gives data consistency in multiple threading, then why Atomic variables are introduced? What is the main difference between Volatile and Atomic variables?
The effect of the volatile keyword is approximately same that each individual read or write operation on that variable is atomic. However, an operation that requires more than one read/write operations, such as i++, which is equivalent to i = i + 1, which does one read and one write — is not atomic, since another thread may write to i between the read and the write.
CAS is fast — much faster than a synchronized block. It’s a building-block for higher level locking & concurrency operations.
For performance-critical code, we should consider & often prefer CAS-based approaches due to their superior performance over ‘synchronized’.
Can’t be more agree with you.
Lokesh,
There is a possibility that 2 threads which are trying to get the incremented value – reach the below statement at same time – which checks for if A = V
– and this statement hold true for both threads .
How this scenario gets handled???
“A = V” will never be true for both threads if first thread modified the value at V. Remember all 3 operations are done as a atomic operation.
Hi Lokesh,
I have two doubts here..
1) You used three variables in total in the example. One variable V is for actual data location where the data lives in the memory.Second is A which is a temporary variable to read the value at V from memory and store for comparison(which is called “current” technically). Third is B which is again a temporary variable to increment the value which has been read(A) and replace the original value in the memory(which is called “next” technically). Does compareAndSet(int current, int next) method in say AtomicInteger class do the same? I mean use all the three locations?
2) You mentioned that all those three operations, i.e. reading V, doing if(V==A) and V==B are atomic. Something can’t be atomic by itself yes? For example to make a block of code atomic, we use Synchronized blocks or classes in java.util.concurrent.locks package. And it is interesting that you mentioned that atomicity is maintained at OS level. Can you please provide an insight into how actually it is done of possible? If both threads read V==A at the same time case. Thanks in advance. Your article is very helpful btw :)
Hi Lokesh,
Thank you for such a nice explanation about CAS.
I have a query on this statement, “But the losers are not punished by suspension of thread. They are free to retry the operation or simply do nothing”.
– What determines whether a thread will retry or do nothing? By ‘operation failed…’ do you an exception is thrown?
It solely depend on thread itself that it will retry or not. e.g. Java AtomicXXX will retry if failed in first attempt. Operation failed means the value which thread was trying to update, didn’t updated; in other words, update operation failed.
Hi Pankaj,
Let me again thank you for simplify CAS. Please clarify some of my queries on CAS
1. “Now thread 2, again retry this operation with values” – In a Concurrent Multi-Thread scenario all the threads(except 1) operation fails and all of them try to re-attempt, which directly increases there ideal time ?
2. What do you think about “CAS” not created as a keyword like “Volatile”?