什么是CAS
CAS 即 compare and swap,比較並交換。
CAS是一種原子操作,同時 CAS 使用樂觀鎖機制。
J.U.C中的很多功能都是建立在 CAS 之上,各種原子類,其底層都用 CAS來實現原子操作。用來解決並發時的安全問題。
並發安全問題
舉一個典型的例子i++
public class AddTest {
public volatile int i;
public void add() {
i++;
}
}
通過javap -c AddTest
可以看到add 方法的字節碼指令:
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
i++
被拆分成了多個指令:
- 執行
getfield
拿到原始內存值; - 執行
iadd
進行加 1 操作; - 執行
putfield
寫把累加后的值寫回內存。
假設一種情況:
- 當
線程 1
執行到iadd
時,由於還沒有執行putfield
,這時候並不會刷新主內存區中的值。 - 此時
線程 2
進入開始運行,剛剛將主內存區的值拷貝到私有內存區。 線程 1
正好執行putfield
,更新主內存區的值,那么此時線程 2
的副本就是舊的了。錯誤就出現了。
如何解決?
最簡單的,在 add 方法加上 synchronized 。
public class AddTest {
public volatile int i;
public synchronized void add() {
i++;
}
}
雖然簡單,並且解決了問題,但是性能表現並不好。
最優的解法應該是使用JDK自帶的CAS方案,如上例子,使用AtomicInteger
類
public class AddIntTest {
public AtomicInteger i;
public void add() {
i.getAndIncrement();
}
}
底層原理
CAS 的原理並不復雜:
- 三個參數,一個當前內存值 V、預期值 A、更新值 B
- 當且僅當預期值 A 和內存值 V 相同時,將內存值修改為 B 並返回 true
- 否則什么都不做,並返回 false
拿 AtomicInteger
類分析,先來看看源碼:
我這里的環境是Java11,如果是Java8這里一些內部的一些命名有些許不同。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
/*
* This class intended to be implemented using VarHandles, but there
* are unresolved cyclic startup dependencies.
*/
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
//...
}
Unsafe
類,該類對一般開發而言,少有用到。
Unsafe
類底層是用 C/C++ 實現的,所以它的方式都是被 native 關鍵字修飾過的。
它可以提供硬件級別的原子操作,如獲取某個屬性在內存中的位置、修改對象的字段值。
關鍵點:
-
AtomicInteger
類存儲的值在value
字段中,而value
字段被volatile
-
在靜態代碼塊中,並且獲取了
Unsafe
實例,獲取了value
字段在內存中的偏移量VALUE
接下回到剛剛的例子:
如上,getAndIncrement()
方法底層利用 CAS 技術保證了並發安全。
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
getAndAddInt()
方法:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
v
通過 getIntVolatile(o, offset)
方法獲取,其目的是獲取 o
在 offset
偏移量的值,其中 o
就是 AtomicInteger
類存儲的值,即value
, offset
內存偏移量的值,即 VALUE
。
重點,weakCompareAndSetInt
就是實現 CAS 的核心方法
- 如果
o
和v
相等,就證明沒有其他線程改變過這個變量,那么就把v
值更新為v + delta
,其中delta
是更新的增量值。 - 反之 CAS 就一直采用自旋的方式繼續進行操作,這一步也是一個原子操作。
分析:
- 設定
AtomicInteger
的原始值為 A,線程 1
和線程 2
各自持有一份副本,值都是 A。
線程 1
通過getIntVolatile(o, offset)
拿到 value 值 A,這時線程 1
被掛起。線程 2
也通過getIntVolatile(o, offset)
方法獲取到 value 值 A,並執行weakCompareAndSetInt
方法比較內存值也為 A,成功修改內存值為 B。- 這時
線程 1
恢復執行weakCompareAndSetInt
方法比較,發現自己手里的值 A 和內存的值 B 不一致,說明該值已經被其它線程提前修改過了。 線程 1
重新執行getIntVolatile(o, offset)
再次獲取 value 值,因為變量 value 被 volatile 修飾,具有可見性,線程A繼續執行weakCompareAndSetInt
進行比較替換,直到成功
CAS需要注意的問題
使用限制
CAS是由CPU支持的原子操作,其原子性是在硬件層面進行保證的,在Java中普通用戶無法直接使用,只能借助atomic
包下的原子類使用,靈活性受限。
但是CAS只能保證單個變量操作的原子性,當涉及到多個變量時,CAS無能為力。
原子性也不一定能保證線程安全,如在Java中需要與volatile
配合來保證線程安全。
ABA 問題
概念
CAS 有一個問題,舉例子如下:
線程 1
從內存位置 V 取出 A- 這時候
線程 2
也從內存位置 V 取出 A - 此時
線程 1
處於掛起狀態,線程 2
將位置 V 的值改成 B,最后再改成 A - 這時候
線程 1
再執行,發現位置 V 的值沒有變化,符合期望繼續執行。
此時雖然線程 1
還是成功了,但是這並不符合我們真實的期望,等於線程 2
狸貓換太子把線程 1
耍了。
這就是所謂的ABA問題
解決方案
引入原子引用,帶版本號的原子操作。
把我們的每一次操作都帶上一個版本號,這樣就可以避免ABA問題的發生。既樂觀鎖的思想。
-
內存中的值每發生一次變化,版本號都更新。
-
在進行CAS操作時,比較內存中的值的同時,也會比較版本號,只有當二者都沒有變化時,才能執行成功。
-
Java中的
AtomicStampedReference
類便是使用版本號來解決ABA問題的。
高競爭下的開銷問題
-
在並發沖突概率大的高競爭環境下,如果CAS一直失敗,會一直重試,CPU開銷較大。
-
針對這個問題的一個思路是引入退出機制,如重試次數超過一定閾值后失敗退出。
-
更重要的是避免在高競爭環境下使用樂觀鎖。