CAS and Atomic classes
Summary
CAS (Compare-and-Swap), i.e. Compare and Replace, is a commonly used technology to implement concurrent algorithms. CAS technology is used in many classes of Java concurrent packages.
case
package com.test.cas; import java.util.concurrent.CountDownLatch; public class VolatileTest4 { private static volatile int race = 0; private static void incr() { race++; } public static void main(String[] args) throws InterruptedException { //CountDownLatch latch = new CountDownLatch(20); for (int i = 0; i < 20; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++) { incr(); } //latch.countDown(); } }).start(); } // Continue to execute the main thread when all is running. //latch.await(); while(Thread.activeCount() >1) { Thread.yield(); } System.out.println("race:" + race); } }
It can be determined that whether the race field is added with or without volitile, there will be problems. The expectation is 2000, not actually. This is because race++ is an operation of several steps, which can not guarantee the atomicity of these steps.
How to change that?
Simply add synchronized.
private synchronized static void incr() { race++; }
Or change to java.util.concurrent.atomic.AtomicInteger
package com.test.cas; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; public class VolatileTest3 { private static AtomicInteger race = new AtomicInteger(0); private static void incr() { race.addAndGet(1); } public static void main(String[] args) throws InterruptedException { //CountDownLatch latch = new CountDownLatch(20); for (int i = 0; i < 20; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++) { incr(); } //latch.countDown(); } }).start(); } // Continue to execute the main thread when all is running. //latch.await(); while(Thread.activeCount() >1) { Thread.yield(); } System.out.println("race:" + race); } }
Why can Atomic Integer?
Tracking its code
private static final Unsafe unsafe = Unsafe.getUnsafe(); public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; }
Follow it in again
You can see the logic of the interface:
- First, get the latest v value (which is the expected value)
- Re-pass compareAndSwapInt parameters: expected value v, new value v+delta, etc.
- If the value of the memory address to be modified is equal to the expected value of v, indicating that it has not been modified by other threads in the middle, the value on the memory address (i.e. the old value) is replaced with the new value, and the modified value (i.e. the old value) is returned successfully, until the end.
- If the value is not equal to the expected v, it means that it has been modified halfway, then repeat 1, 2, 3 until successful.
(To add the meaning of interface Object and offset field, we should modify the offset field of o object. As for why offset is called, I estimate that the object occupies a certain amount of memory, and use offset to determine which offset the field has in memory.)
CAS detailed explanation
CAS is the abbreviation of the English word Compare AndSwap, which means: compare and replace. CAS requires three operands: memory address V, old expected value A, and target value B to be updated.
When the CAS instruction is executed, if and only if the value of memory address V is equal to the expected value A, the value of memory address V is changed to B, otherwise nothing is done. The whole operation of comparison and substitution is an atomic operation.
Disadvantages of CAS
Although CAS solves the problem of atomic operation efficiently, there are still three major problems in CAS.
- Cycle time is long and expensive.
- Only one atomic operation of shared variables can be guaranteed.
- ABA problem.
Note: Continuous cycling until success is sun.misc.Unsafe API, not the CAS underlying instruction itself, the underlying operation once, if a new value and expectations are different, do not try again. Unsafe Java class is a self-implemented loop attempt, and compareAndSwapXXX only tries once. Only getAndAddXXX and getAndSetXXX realize the loop attempt.
Loop actual long overhead
If CAS fails, it will keep trying. If CAS is unsuccessful for a long time, it may bring a lot of overhead to CPU.
Only one atomic operation of shared variables can be guaranteed.
When operating on a shared variable, we can use cyclic CAS to ensure atomic operation, but when operating on multiple shared variables, cyclic CAS can not guarantee the atomicity of the operation. At this time, we can use locks to ensure atomicity.
ABA problem
If the first value read by memory address V is A and the value is still A when it is ready for assignment, can we say that its value has not been changed by other threads?
If its value was changed to B during this period and then to A, the CAS operation would mistakenly assume that it has never been changed. This vulnerability is called the "ABA" problem of CAS operations. To solve this problem, Java concurrent packages provide a tagged atomic reference class, Atomic Stamped Reference, which can ensure the correctness of CAS by controlling the version of variable values. Therefore, before using CAS, we should consider whether the "ABA" problem will affect the correctness of program concurrency. If ABA problem needs to be solved, the traditional mutually exclusive synchronization may be more efficient than the atomic class.