前言
說到CAS(CompareAndSwap),不得不先說一說悲觀鎖和樂觀鎖,因為CAS是樂觀鎖思想的一種實現。
悲觀鎖:總是很悲觀的認為,每次拿數據都會有其他線程並發執行,所以每次都會進行加鎖,用完之后釋放鎖,其他的線程才能拿到鎖,進而拿到資源進行操作。java中的synchronized和ReentrantLock等獨占鎖就是悲觀鎖思想的實現。
樂觀鎖:總是很樂觀認為,自己拿到數據操作的時候,沒有其他線程來並發操作,等自己操作結束要更新數據時,判斷自己對數據操作的期間有沒有其他線程進行操作,如果有,則進行重試,直到操作變更成功。樂觀鎖常使用CAS和版本號機制來實現。java中java.util.atomic包下的原子類都是基於CAS實現的。
一、什么是CAS
CAS指CompareAndSwap,顧名思義,先比較后交換。比較什么?交換什么呢?
CAS中有三個變量:內存地址V,期待值A, 更新值B。
當且僅當內存地址V對應的值與期待值A時相等時,將內存地址V對應的值更換為B。
二、atomic包
有了悲觀鎖,樂觀鎖的知識,讓我們走進java.util.atomic包,看一看java中CAS的實現。

這就是java.util.atomic包下的類,我們着重看AtomicInteger源碼(其他的都是一樣的思想實現的)
然后思考CAS有什么弊端?如何解決弊端?有什么優缺點?
2.1、走進AtomicInteger源碼
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 使用Unsafe.compareAndSwapInt進行原子更新操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
//value對應的存儲地址偏移量
private static final long valueOffset;
static {
try {
//使用反射及unsafe.objectFieldOffset拿到value字段的內存地址偏移量,這個值是固定不變的
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//volatile修飾的共享變量
private volatile int value;
//..........
}
上面的代碼其實就是為了初始化內存值對應的內存地址偏移量valueOffset,方便后續執行CAS操作時使用。因為這個值一旦初始化,就不會更改,所以使用static final 修飾。
我們可以看到value使用了volatile修飾,上一篇9龍詳細詳解了JMM,其中也說了volatile的語義,不了解的小伙伴可以先去看一看。
我們都知道如果進行value++操作,並發下是不安全的。上一篇中我們也通過例子證明了volatile只能保證可見性,不能保證原子性。因為value++本身不是原子操作,value++分了三步,先拿到value的值,進行+1,再賦值回value。
2.2、compareAndSwapXxx
我們先看一看AtomicInteger提供的CAS操作。
/**
* 原子地將value設置為update,如果valueOffset對應的值與expect相等時
*
* @param expect 期待值
* @param update 更新值
* @return 如果更新成功,返回true;在valueOffset對應的值與expect不相等時返回false
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
我們已經知道CAS的原理,那來看看下面的測試。你知道輸出的結果是多少嗎?評論區給出你的答案吧。
public class AtomicIntegerTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.compareAndSet(0, 1);
atomicInteger.compareAndSet(2, 1);
atomicInteger.compareAndSet(1, 3);
atomicInteger.compareAndSet(2, 4);
System.out.println(atomicInteger.get());
}
}
Unsafe提供了三個原子更新的方法。
關於Unsafe類,因為java不支持直接操作底層硬件資源,如分配內存等。如果你使用unsafe開辟的內存,是不被JVM垃圾回收管理,需要自己管理,容易造成內存泄漏等。
2.3、AtomicInteger的原子自增方法
我們上面說了,value++不是原子操作,不能在並發下使用。我們來看看AtomicInteger提供的原子++操作。
/**
* 原子地對value進行+1操作
*
* @return 返回更新后的值
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/**
* unsafe提供的方法
* var1 更改的目標對象
* var2 目標對象的共享字段對應的內存地址偏移量valueOffset
* var4 需要在原value上增加的值
* @return 返回未更新前的值
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
//期待值
int var5;
do {
//獲取valueOffset對應的value的值,支持volatile load
var5 = this.getIntVolatile(var1, var2);
//如果原子更新失敗,則一直重試,直到成功。
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
我們看到CAS只能原子的更新一個值,如果我們要原子更新多個值,CAS可以做到嗎?答案是可以的。
2.4、AtomicReference
如果要原子地更新多個值,就需要使用AtomicReference。其使用的是compareAndSwapObject方法。可以將多個值封裝到一個對象中,原子地更換對象來實現原子更新多個值。
public class MultiValue {
private int value1;
private long value2;
private Integer value3;
public MultiValue(int value1, long value2, Integer value3) {
this.value1 = value1;
this.value2 = value2;
this.value3 = value3;
}
}
public class AtomicReferenceTest {
public static void main(String[] args) {
MultiValue multiValue1 = new MultiValue(1, 1, 1);
MultiValue multiValue2 = new MultiValue(2, 2, 2);
MultiValue multiValue3 = new MultiValue(3, 3, 3);
AtomicReference<MultiValue> atomicReference = new AtomicReference<>();
//因為構造AtomicReference時,沒有使用有參構造函數,所以value默認值是null
atomicReference.compareAndSet(null, multiValue1);
System.out.println(atomicReference.get());
atomicReference.compareAndSet(multiValue1, multiValue2);
System.out.println(atomicReference.get());
atomicReference.compareAndSet(multiValue2, multiValue3);
System.out.println(atomicReference.get());
}
}
//輸出結果
//MultiValue{value1=1, value2=1, value3=1}
//MultiValue{value1=2, value2=2, value3=2}
//MultiValue{value1=3, value2=3, value3=3}
我們再看一看AtomicReference的compareAndSet方法。
注意:這里的比較都是使用==而非equals方法。所以最好封裝的MultiValue不要提供set方法。
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
2.5、CAS的ABA問題
假設你的賬戶上有100塊錢,你要給女票轉50塊錢。
我們使用CAS進行原子更新賬戶余額。由於某種原因,你第一次點擊轉賬出現錯誤,你以為沒有發起轉賬請求,這時候你又點擊了一次。系統開啟了兩個線程進行轉賬操作,第一個線程進行CAS比較,發現你的賬戶上預期是100塊錢,實際也有100塊錢,這時候轉走了50,需要設置為100 - 50 = 50 元,這時賬戶余額為50
第一個線程操作成功了,第二個線程由於某種原因阻塞住了;這時候,你的家人又給你轉了50塊錢,並且轉賬成功。那你賬戶上現在又是100塊錢;
太巧了,第二個線程被喚醒了,發現你的賬戶是100塊錢,跟預期的100是相等的,這時候又CAS為50。大兄弟,哭慘了,你算算,正確的場景你要有多少錢?這就是CAS存在的ABA問題。
public class AtomicIntegerABA {
private static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
//線程1
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
atomicInteger.compareAndSet(100, 50);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
});
//線程2
executorService.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
atomicInteger.compareAndSet(50, 100);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
});
//線程3
executorService.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
atomicInteger.compareAndSet(100, 50);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
});
executorService.shutdown();
}
}
//輸出結果
//pool-1-thread-1 - 100
//pool-1-thread-1 - 50
//pool-1-thread-2 - 50
//pool-1-thread-2 - 100
//pool-1-thread-3 - 100
//pool-1-thread-3 - 50
大家心想,靠,這不是坑嗎?那還用。。。。。。。。。。。。。。冷靜,冷靜。你能想到的問題,jdk都能想到。atomic包提供了一個AtomicStampedReference
2.6、AtomicStampedReference
看名字是不是跟AtomicReference很像啊,其實就是在AtomicReference上加上了一個版本號,每次操作都對版本號進行自增,那每次CAS不僅要比較value,還要比較stamp,當且僅當兩者都相等,才能夠進行更新。
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
//定義了內部靜態內部類Pair,將構造函數初始化的值與版本號構造一個Pair對象。
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
//所以我們之前的value就對應為現在的pair
private volatile Pair<V> pair;
讓我們來看一看它的CAS方法。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
//只有在舊值與舊版本號都相同的時候才會更新為新值,新版本號
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
還是上面轉賬的例子,我們使用AtomicStampedReference來看看是否解決了呢。
public class AtomicStampedReferenceABA {
/**
* 初始化賬戶中有100塊錢,版本號對應0
*/
private static AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(100, 0);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
int[] result = new int[1];
//線程1
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
//將100更新為50,版本號+1
atomicInteger.compareAndSet(100, 50, 0, 1);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
});
//線程2
executorService.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
//將50更新為100,版本號+1
atomicInteger.compareAndSet(50, 100, 1, 2);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
});
//線程3
executorService.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
//此線程還是以為沒有其他線程進行過更改,所以舊版本號還是0
atomicInteger.compareAndSet(100, 50, 0, 1);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
});
executorService.shutdown();
}
}
//輸出結果
//pool-1-thread-1 - 100
//pool-1-thread-1 - 50
//pool-1-thread-2 - 50
//pool-1-thread-2 - 100
//pool-1-thread-3 - 100
//pool-1-thread-3 - 100
媽媽再也不用擔心我的錢少了。
三、總結
本篇詳細講解了CAS的原理,CAS可以進行原子更新一個值(包括對象),主要用於讀多寫少的場景,如原子自增操作,如果多線程調用,在CAS失敗之后,會死循環一直重試,直到更新成功。這種情況是很耗CPU資源的,雖然沒有鎖,但循環的自旋可能比鎖的代價還高。同時存在ABA問題,但AtomicStampedReference通過加入版本號機制已經解決。其實對於atomic包,jdk1.8新增的LongAdder,效率比AtomicLong高,9龍還未涉足,以后肯定會品一品。J.U.C(java.util.concurrent)包中大量使用了CAS,ConcurrentHashMap也使用到,如果不了解CAS,怎么入手J.U.C呢。
各位看官,如果覺得9龍的文章對你有幫助,求點贊,求關注。如果轉載請注明出處。
參考鏈接: