您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關java中CAS是什么的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
Java并發編程系列番外篇C A S(Compare and swap)
,文章風格依然是圖文并茂,通俗易懂,讓讀者們也能與面試官瘋狂對線。
C A S
作為并發編程必不可少的基礎知識,面試時C A S
也是個高頻考點,所以說C A S
是必知必會,本文將帶讀者們深入理解C A S
。
C A S(compareAndSwap)
也叫比較交換,是一種無鎖原子算法,映射到操作系統就是一條cmpxchg
硬件匯編指令(保證原子性),其作用是讓C P U
將內存值更新為新值,但是有個條件,內存值必須與期望值相同,并且C A S
操作無需用戶態與內核態切換,直接在用戶態對內存進行讀寫操作(意味著不會阻塞/線程上下文切換)。
它包含3
個參數C A S(V,E,N)
,V
表示待更新的內存值,E
表示預期值,N
表示新值,當 V
值等于E
值時,才會將V
值更新成N
值,如果V
值和E
值不等,不做更新,這就是一次C A S
的操作。
簡單說,C A S
需要你額外給出一個期望值,也就是你認為這個變量現在應該是什么樣子的,如果變量不是你想象的那樣,說明它已經被別人修改過了,你只需要重新讀取,設置新期望值,再次嘗試修改就好了。
原子性是指一個或者多個操作在C P U
執行的過程中不被中斷的特性,要么執行,要不執行,不能執行到一半(不可被中斷的一個或一系列操作)。
為了保證C A S
的原子性,C P U
提供了下面兩種方式
總線鎖定
緩存鎖定
總線(B U S
)是計算機組件間的傳輸數據方式,也就是說C P U
與其他組件連接傳輸數據,就是靠總線完成的,比如C P U
對內存的讀寫。
總線鎖定是指C P U
使用了總線鎖,所謂總線鎖就是使用C P U
提供的LOCK#
信號,當C P U
在總線上輸出LOCK#
信號時,其他C P U
的總線請求將被阻塞。
總線鎖定方式雖然保證了原子性,但是在鎖定期間,會導致大量阻塞,增加系統的性能開銷,所以現代C P U
為了提升性能,通過鎖定范圍縮小的思想設計出了緩存行鎖定(緩存行是C P U
高速緩存存儲的最小單位)。
所謂緩存鎖定是指C P U
對緩存行進行鎖定,當緩存行中的共享變量回寫到內存時,其他C P U
會通過總線嗅探機制感知該共享變量是否發生變化,如果發生變化,讓自己對應的共享變量緩存行失效,重新從內存讀取最新的數據,緩存鎖定是基于緩存一致性機制來實現的,因為緩存一致性機制會阻止兩個以上C P U
同時修改同一個共享變量(現代C P U
基本都支持和使用緩存鎖定機制)。
C A S
和鎖都解決了原子性問題,和鎖相比沒有阻塞、線程上下文你切換、死鎖,所以C A S
要比鎖擁有更優越的性能,但是C A S
同樣存在缺點。
C A S
的問題如下
只能保證一個共享變量的原子操作
自旋時間太長(建立在自旋鎖的基礎上)
ABA
問題
C A S
只能針對一個共享變量使用,如果多個共享變量就只能使用鎖了,當然如果你有辦法把多個變量整成一個變量,利用C A S
也不錯,例如讀寫鎖中state
的高低位。
當一個線程獲取鎖時失敗,不進行阻塞掛起,而是間隔一段時間再次嘗試獲取,直到成功為止,這種循環獲取的機制被稱為自旋鎖(spinlock
)。
自旋鎖好處是,持有鎖的線程在短時間內釋放鎖,那些等待競爭鎖的線程就不需進入阻塞狀態(無需線程上下文切換/無需用戶態與內核態切換),它們只需要等一等(自旋),等到持有鎖的線程釋放鎖之后即可獲取,這樣就避免了用戶態和內核態的切換消耗。
自旋鎖壞處顯而易見,線程在長時間內持有鎖,等待競爭鎖的線程一直自旋,即CPU一直空轉,資源浪費在毫無意義的地方,所以一般會限制自旋次數。
最后來說自旋鎖的實現,實現自旋鎖可以基于C A S
實現,先定義lockValue
對象默認值1
,1
代表鎖資源空閑,0
代表鎖資源被占用,代碼如下
public class SpinLock { //lockValue 默認值1 private AtomicInteger lockValue = new AtomicInteger(1); //自旋獲取鎖 public void lock(){ // 循環檢測嘗試獲取鎖 while (!tryLock()){ // 空轉 } } //獲取鎖 public boolean tryLock(){ // 期望值1,更新值0,更新成功返回true,更新失敗返回false return lockValue.compareAndSet(1,0); } //釋放鎖 public void unLock(){ if(!lockValue.compareAndSet(1,0)){ throw new RuntimeException("釋放鎖失敗"); } } }
上面定義了AtomicInteger
類型的lockValue
變量,AtomicInteger
是Java
基于C A S
實現的Integer
原子操作類,還定義了3個函數lock、tryLock、unLock
tryLock函數-獲取鎖
期望值1,更新值0
C A S
更新
如果期望值與lockValue
值相等,則lockValue
值更新為0
,返回true
,否則執行下面邏輯
如果期望值與lockValue
值不相等,不做任何更新,返回false
unLock函數-釋放鎖
期望值0
,更新值1
C A S
更新
如果期望值與lockValue
值相等,則lockValue
值更新為1
,返回true
,否則執行下面邏輯
如果期望值與lockValue
值不相等,不做任何更新,返回false
lock函數-自旋獲取鎖
執行tryLock
函數,返回true
停止,否則一直循環
從上圖可以看出,只有tryLock
成功的線程(把lockValue
更新為0
),才會執行代碼塊,其他線程個tryLock
自旋等待lockValue
被更新成1
,tryLock
成功的線程執行unLock
(把lockValue
更新為1
),自旋的線程才會tryLock
成功。
C A S
需要檢查待更新的內存值有沒有被修改,如果沒有則更新,但是存在這樣一種情況,如果一個值原來是A
,變成了B
,然后又變成了A
,在C A S
檢查的時候會發現沒有被修改。
假設有兩個線程,線程1
讀取到內存值A
,線程1
時間片用完,切換到線程2
,線程2
也讀取到了內存值A
,并把它修改為B
值,然后再把B
值還原到A
值,簡單說,修改次序是A->B->A
,接著線程1
恢復運行,它發現內存值還是A
,然后執行C A S
操作,這就是著名的ABA
問題,但是好像又看不出什么問題。
只是簡單的數據結構,確實不會有什么問題,如果是復雜的數據結構可能就會有問題了(使用AtomicReference
可以把C A S
使用在對象上),以鏈表數據結構為例,兩個線程通過C A S
去刪除頭節點,假設現在鏈表有A->B
節點
線程1
刪除A
節點,B
節點成為頭節點,正要執行C A S(A,A,B)
時,時間片用完,切換到線程2
線程2
刪除A、B
節點
線程2
加入C、A
節點,鏈表節點變成A->C
線程1
重新獲取時間片,執行C A S(A,A,B)
丟失C
節點
要解決A B A
問題也非常簡單,只要追加版本號即可,每次改變時加1
,即A —> B —> A
,變成1A —> 2B —> 3A
,在Java
中提供了AtomicStampedRdference
可以實現這個方案(面試只要問了C A S
,就一定會問ABA
,這塊一定要搞明白)。
感謝各位的閱讀!關于“java中CAS是什么”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。