什么是CAS
CAS
即Compare And Swap
的縮寫,翻譯成中文就是比較并交換,其作用是讓CPU比較內存中某個值是否和預期的值相同,如果相同則將這個值更新為新值,不相同則不做更新,也就是CAS是原子性的操作(讀和寫兩者同時具有原子性),其實現方式是通過借助C/C++
調用CPU指令完成的,所以效率很高。CAS
的原理很簡單,這里使用一段Java
代碼來描述
public boolean compareAndSwap(int value, int expect, int update) {
// 如果內存中的值value和期望值expect一樣 則將值更新為新值updateif (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/…