什麼是CAS
CAS
即Compare And Swap
的縮寫,翻譯成中文就是比較並交換,其作用是讓CPU比較記憶體中某個值是否和預期的值相同,如果相同則將這個值更新為新值,不相同則不做更新,也就是CAS是原子性的操作(讀和寫兩者同時具有原子性),其實現方式是通過藉助C/C++
呼叫CPU指令完成的,所以效率很高。
CAS
的原理很簡單,這裡使用一段Java
程式碼來描述
public boolean compareAndSwap(int value, int expect, int update) {
// 如果記憶體中的值value和期望值expect一樣 則將值更新為新值update
if (value == expect) {
value = update;
return true;
} else {
return false;
}
}
複製程式碼
大致過程是將記憶體中的值、我們的期望值、新值交給CPU進行運算,如果記憶體中的值和我們的期望值相同則將值更新為新值,否則不做任何操作。這個過程是在CPU中完成的,這裡不好描述CPU的工作過程,就拿Java程式碼來描述了。
Unsafe原始碼分析
Java是在Unsafe(sun.misc.Unsafe)
類實現CAS
的操作,而我們知道Java是無法直接訪問作業系統底層的API的(原因是Java的跨平臺性限制了Java不能和作業系統耦合),所以Java並沒有在Unsafe
類直接實現CAS
的操作,而是通過**JDI(Java Native Interface)**本地呼叫C/C++
語言來實現CAS
操作的。
Unsafe
有很多個CAS
操作的相關方法,這裡舉例幾個
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
複製程式碼
我們拿public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
進行分析,這個方法是比較記憶體中的一個值(整型)和我們的期望值(var4)是否一樣,如果一樣則將記憶體中的這個值更新為var5
,引數中的var1
是值所在的物件,var2
是值在物件(var1)中的記憶體偏移量,引數var1和引數var2是為了定位出值所在記憶體的地址。
- 將物件引用、值在物件中的偏移量、期望的值和欲更新的新值傳遞給
Unsafe.cpp
- 如果值更新成功則返回
true
給開發者,沒有更新則返回false
Unsafe.cpp在這裡發揮的作用有:
- 接受從
Unsafe
傳遞過來的物件引用、偏移量、期望的值和欲更新的新值,根據物件引用和偏移量計算出值的地址,然後將值的地址、期望的值、欲更新的新值傳遞給CPU - 如果值更新成功則返回
true
給Unsafe.java
,沒有更新則返回false
CPU在這裡發揮的作用:
- 接受從
Unsafe.cpp
傳遞過來的地址、期望的值和欲更新的新值,執行指令cmpxchg
,比較地址中的值是否和期望的值一樣,一樣則將值更新為新的值,不一樣則不做任何操作 - 將操作結果返回給
Unsafe.cpp
CAS的缺點
CAS
雖然高效的實現了原子性操作,但是也存在一些缺點,主要表現在以下三個方面。
ABA問題
在多執行緒場景下CAS
會出現ABA
問題,關於ABA問題這裡簡單科普下,例如有2個執行緒同時對同一個值(初始值為A)進行CAS操作,這三個執行緒如下
- 執行緒1,期望值為A,欲更新的值為B
- 執行緒2,期望值為A,欲更新的值為B
執行緒1
搶先獲得CPU時間片,而執行緒2
因為其他原因阻塞了,執行緒1
取值與期望的A值比較,發現相等然後將值更新為B,然後這個時候出現了執行緒3
,期望值為B,欲更新的值為A,執行緒3取值與期望的值B比較,發現相等則將值更新為A,此時執行緒2
從阻塞中恢復,並且獲得了CPU時間片,這時候執行緒2
取值與期望的值A比較,發現相等則將值更新為B,雖然執行緒2
也完成了操作,但是執行緒2
並不知道值已經經過了A->B->A
的變化過程。
ABA
問題帶來的危害:
小明在提款機,提取了50元,因為提款機問題,有兩個執行緒,同時把餘額從100變為50
執行緒1(提款機):獲取當前值100,期望更新為50,
執行緒2(提款機):獲取當前值100,期望更新為50,
執行緒1成功執行,執行緒2某種原因block了,這時,某人給小明匯款50
執行緒3(預設):獲取當前值50,期望更新為100,
這時候執行緒3成功執行,餘額變為100,
執行緒2從Block中恢復,獲取到的也是100,compare之後,繼續更新餘額為50!!!
此時可以看到,實際餘額應該為100(100-50+50),但是實際上變為了50(100-50+50-50)這就是ABA問題帶來的成功提交。
解決方法:
在變數前面加上版本號,每次變數更新的時候變數的版本號都+1
,即A->B->A
就變成了1A->2B->3A
。
迴圈時間長開銷大
如果CAS
操作失敗,就需要迴圈進行CAS
操作(迴圈同時將期望值更新為最新的),如果長時間都不成功的話,那麼會造成CPU極大的開銷。
這種迴圈也稱為自旋
解決方法: 限制自旋次數,防止進入死迴圈。
只能保證一個共享變數的原子操作
CAS
的原子操作只能針對一個共享變數。
解決方法:
如果需要對多個共享變數進行操作,可以使用加鎖方式(悲觀鎖)保證原子性,或者可以把多個共享變數合併成一個共享變數進行CAS
操作。
CAS的應用
我們知道CAS
操作並不會鎖住共享變數,也就是一種非阻塞的同步機制,CAS
就是樂觀鎖的實現。
- 樂觀鎖 樂觀鎖總是假設最好的情況,每次去運算元據都認為不會被別的執行緒修改資料,所以在每次運算元據的時候都不會給資料加鎖,即線上程對資料進行操作的時候,別的執行緒不會阻塞仍然可以對資料進行操作,只有在需要更新資料的時候才會去判斷資料是否被別的執行緒修改過,如果資料被修改過則會拒絕操作並且返回錯誤資訊給使用者。
- 悲觀鎖 悲觀鎖總是假設最壞的情況,每次去運算元據時候都認為會被的執行緒修改資料,所以在每次運算元據的時候都會給資料加鎖,讓別的執行緒無法操作這個資料,別的執行緒會一直阻塞直到獲取到這個資料的鎖。這樣的話就會影響效率,比如當有個執行緒發生一個很耗時的操作的時候,別的執行緒只是想獲取這個資料的值而已都要等待很久。
Java
利用CAS
的樂觀鎖、原子性的特性高效解決了多執行緒的安全性問題,例如JDK1.8中的集合類ConcurrentHashMap
、關鍵字volatile
、ReentrantLock
等。
參考
JAVA CAS原理深度分析
Java CAS 原理分析
什麼是ABA問題?
原文地址:ddnd.cn/2019/03/13/…