什麼是CAS
(1)CAS(compare and swap) 比較並替換,比較和替換是執行緒併發演算法時用到的一種技術
(2)CAS是原子操作,保證併發安全,而不是保證併發同步
(3)CAS是CPU的一個指令
(4)CAS是非阻塞的、輕量級的樂觀鎖
為什麼說CAS是樂觀鎖
樂觀鎖,嚴格來說並不是鎖,通過原子性來保證資料的同步,比如說資料庫的樂觀鎖,通過版本控制來實現等,所以CAS不會保證執行緒同步。樂觀的認為在資料更新期間沒有其他執行緒影響
CAS原理
CAS(compare and swap) 比較並替換,就是將記憶體值更新為需要的值,但是有個條件,記憶體值必須與期望值相同。舉個例子,期望值 E、記憶體值M、更新值U,當E == M的時候將M更新為U。
CAS應用
由於CAS是CPU指令,我們只能通過JNI與作業系統互動,關於CAS的方法都在sun.misc包下Unsafe的類裡 java.util.concurrent.atomic包下的原子類等通過CAS來實現原子操作。
CAS舉例
/**
* Created by Dell on 2018/2/6.
*/
public class CasLock {
private static final CountDownLatch latch = new CountDownLatch(5);
private static AtomicInteger i = new AtomicInteger(0);
private static int p = 0;
public static void main(String[] args) throws InterruptedException {
long time = System.currentTimeMillis();
ExecutorService pool = Executors.newFixedThreadPool(5);
for(int j = 0; j < 5; j++) {
pool.execute(new Runnable() {
public void run() {
for(int k = 0; k < 10000; k++) {
p++; //不是原子操作
i.getAndIncrement();//呼叫原子類加1
}
latch.countDown();
}
});
}
latch.await();//保證所有子執行緒執行完成
System.out.println(System.currentTimeMillis() - time);
System.out.println("p=" + p);
System.out.println("i=" + i);
pool.shutdown();
}
}
複製程式碼
輸出結果
"C:\Program Files\Java\jdk1.8.0_91\bin\java" ...
8
p=43204//結果不正確
i=50000
Process finished with exit code 0
複製程式碼
根據結果我們發現,由於多執行緒非同步進行p++操作,導致結果不正確。
為什麼p++的記過不正確呢?比如兩個執行緒讀到p的值為1,然後做加1操作,這時候p的值是2,而不是3
而變數i的結果卻是對的,這就要歸功於CAS,下面我們具體看一下原子類。
CAS指令和具體原始碼
原子類例如AtomicInteger裡的方法都很簡單,大家看一看都能懂,我們具體看下getAndIncrement方法。下面貼出程式碼:
//該方法功能是Interger型別加1
public final int getAndIncrement() {
//主要看這個getAndAddInt方法
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//var1 是this指標
//var2 是地址偏移量
//var4 是自增的數值,是自增1還是自增N
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//獲取記憶體值,這是記憶體值已經是舊的,假設我們稱作期望值E
var5 = this.getIntVolatile(var1, var2);
//compareAndSwapInt方法是重點,
//var5是期望值,var5 + var4是要更新的值
//這個操作就是呼叫CAS的JNI,每個執行緒將自己記憶體裡的記憶體值M
//與var5期望值E作比較,如果相同將記憶體值M更新為var5 + var4,否則做自旋操作
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
複製程式碼
解釋一下getAndAddInt方法的流程
假設有以下情景:
1.A、B兩個執行緒
2.jvm主記憶體的值1,A、B工作記憶體的值為1(工作記憶體會拷貝一份主記憶體的值)
3.當前期望值為1,做加1操作
4.此時var5 = 1, var4 = 1,
(1)A執行緒將var5與工作記憶體值M比較,比較var5是否等於1
(2)如果相同則將工作記憶體值修改為var5+var4 既修改為2並同步到主記憶體,此時this指標裡,示例變數value的值就是2,結束迴圈
(3)如果不相同則其B執行緒修改了主記憶體的值,說明B執行緒已經先於A執行緒做了加1操作,A執行緒沒有更新成功需要繼續迴圈,注意此時var5更新為新的記憶體值,假設當前的記憶體值是2,那麼此時var5 = 2, var5 + var4 = 3,重複上述步驟直到成功
下面是compareAndSwapInt本地方法的原始碼,可以看到使用cmpxchg指令實現CAS,在效率上有不錯的表現。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
複製程式碼
CAS優缺點
- 優點
非阻塞的輕量級的樂觀鎖,通過CPU指令實現,在資源競爭不激烈的情況下效能高,相比synchronized重量鎖,synchronized會進行比較複雜的加鎖,解鎖和喚醒操作。 - 缺點
(1)ABA問題 執行緒C、D,執行緒D將A修改為B後又修改為A,此時C執行緒以為A沒有改變過,java的原子類AtomicStampedReference,通過控制變數值的版本來保證CAS的正確性。
(2)自旋時間過長,消耗CPU資源, 如果資源競爭激烈,多執行緒自旋長時間消耗資源。
CAS總結
CAS不僅是樂觀鎖,是種思想,我們也可以在日常專案中通過類似CAS的操作保證資料安全,但並不是所有場合都適合,曾看過帖子說,能用synchronized就不要用CAS,除非遇到效能瓶頸,因為CAS會讓程式碼可讀性變差,這句話看大家怎麼理解了。