樂觀鎖與悲觀鎖總結
881
2025-04-02
前言
java并發(fā)編程系列番外篇C A S(Compare and swap),文章風格依然是圖文并茂,通俗易懂,讓讀者們也能與面試官瘋狂對線。
C A S作為并發(fā)編程必不可少的基礎知識,面試時C A S也是個高頻考點,所以說C A S是必知必會,本文將帶讀者們深入理解C A S。
大綱
C A S基本概念
C A S(compareAndSwap)也叫比較交換,是一種無鎖原子算法,映射到操作系統(tǒng)就是一條cmpxchg硬件匯編指令(保證原子性),其作用是讓C P U將內存值更新為新值,但是有個條件,內存值必須與期望值相同,并且C A S操作無需用戶態(tài)與內核態(tài)切換,直接在用戶態(tài)對內存進行讀寫操作(意味著不會阻塞/線程上下文切換)。
它包含3個參數(shù)C A S(V,E,N),V表示待更新的內存值,E表示預期值,N表示新值,當?V值等于E值時,才會將V值更新成N值,如果V值和E值不等,不做更新,這就是一次C A S的操作。
簡單說,C A S需要你額外給出一個期望值,也就是你認為這個變量現(xiàn)在應該是什么樣子的,如果變量不是你想象的那樣,說明它已經(jīng)被別人修改過了,你只需要重新讀取,設置新期望值,再次嘗試修改就好了。
C A S如何保證原子性
原子性是指一個或者多個操作在C P U執(zhí)行的過程中不被中斷的特性,要么執(zhí)行,要不執(zhí)行,不能執(zhí)行到一半(不可被中斷的一個或一系列操作)。
為了保證C A S的原子性,C P U提供了下面兩種方式
總線鎖定
緩存鎖定
總線鎖定
總線(B U S)是計算機組件間的傳輸數(shù)據(jù)方式,也就是說C P U與其他組件連接傳輸數(shù)據(jù),就是靠總線完成的,比如C P U對內存的讀寫。
總線鎖定是指C P U使用了總線鎖,所謂總線鎖就是使用C P U提供的LOCK#信號,當C P U在總線上輸出LOCK#信號時,其他C P U的總線請求將被阻塞。
緩存鎖定
總線鎖定方式雖然保證了原子性,但是在鎖定期間,會導致大量阻塞,增加系統(tǒng)的性能開銷,所以現(xiàn)代C P U為了提升性能,通過鎖定范圍縮小的思想設計出了緩存行鎖定(緩存行是C P U高速緩存存儲的最小單位)。
所謂緩存鎖定是指C P U對緩存行進行鎖定,當緩存行中的共享變量回寫到內存時,其他C P U會通過總線嗅探機制感知該共享變量是否發(fā)生變化,如果發(fā)生變化,讓自己對應的共享變量緩存行失效,重新從內存讀取最新的數(shù)據(jù),緩存鎖定是基于緩存一致性機制來實現(xiàn)的,因為緩存一致性機制會阻止兩個以上C P U同時修改同一個共享變量(現(xiàn)代C P U基本都支持和使用緩存鎖定機制)。
C A S的問題
C A S和鎖都解決了原子性問題,和鎖相比沒有阻塞、線程上下文你切換、死鎖,所以C A S要比鎖擁有更優(yōu)越的性能,但是C A S同樣存在缺點。
C A S的問題如下
只能保證一個共享變量的原子操作
自旋時間太長(建立在自旋鎖的基礎上)
ABA問題
只能保證一個共享變量原子操作
C A S只能針對一個共享變量使用,如果多個共享變量就只能使用鎖了,當然如果你有辦法把多個變量整成一個變量,利用C A S也不錯,例如讀寫鎖中state的高低位。
自旋時間太長
當一個線程獲取鎖時失敗,不進行阻塞掛起,而是間隔一段時間再次嘗試獲取,直到成功為止,這種循環(huán)獲取的機制被稱為自旋鎖(spinlock)。
自旋鎖好處是,持有鎖的線程在短時間內釋放鎖,那些等待競爭鎖的線程就不需進入阻塞狀態(tài)(無需線程上下文切換/無需用戶態(tài)與內核態(tài)切換),它們只需要等一等(自旋),等到持有鎖的線程釋放鎖之后即可獲取,這樣就避免了用戶態(tài)和內核態(tài)的切換消耗。
自旋鎖壞處顯而易見,線程在長時間內持有鎖,等待競爭鎖的線程一直自旋,即CPU一直空轉,資源浪費在毫無意義的地方,所以一般會限制自旋次數(shù)。
最后來說自旋鎖的實現(xiàn),實現(xiàn)自旋鎖可以基于C A S實現(xiàn),先定義lockValue對象默認值1,1代表鎖資源空閑,0代表鎖資源被占用,代碼如下
public?class?SpinLock?{
//lockValue?默認值1
private?AtomicInteger?lockValue?=?new?AtomicInteger(1);
//自旋獲取鎖
public?void?lock(){
//?循環(huán)檢測嘗試獲取鎖
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實現(xiàn)的Integer原子操作類,還定義了3個函數(shù)lock、tryLock、unLock
tryLock函數(shù)-獲取鎖
期望值1,更新值0
C A S更新
如果期望值與lockValue值相等,則lockValue值更新為0,返回true,否則執(zhí)行下面邏輯
如果期望值與lockValue值不相等,不做任何更新,返回false
unLock函數(shù)-釋放鎖
期望值0,更新值1
C A S更新
如果期望值與lockValue值相等,則lockValue值更新為1,返回true,否則執(zhí)行下面邏輯
如果期望值與lockValue值不相等,不做任何更新,返回false
lock函數(shù)-自旋獲取鎖
執(zhí)行tryLock函數(shù),返回true停止,否則一直循環(huán)
從上圖可以看出,只有tryLock成功的線程(把lockValue更新為0),才會執(zhí)行代碼塊,其他線程個tryLock自旋等待lockValue被更新成1,tryLock成功的線程執(zhí)行unLock(把lockValue更新為1),自旋的線程才會tryLock成功。
ABA問題
C A S需要檢查待更新的內存值有沒有被修改,如果沒有則更新,但是存在這樣一種情況,如果一個值原來是A,變成了B,然后又變成了A,在C A S檢查的時候會發(fā)現(xiàn)沒有被修改。
假設有兩個線程,線程1讀取到內存值A,線程1時間片用完,切換到線程2,線程2也讀取到了內存值A,并把它修改為B值,然后再把B值還原到A值,簡單說,修改次序是A->B->A,接著線程1恢復運行,它發(fā)現(xiàn)內存值還是A,然后執(zhí)行C A S操作,這就是著名的ABA問題,但是好像又看不出什么問題。
只是簡單的數(shù)據(jù)結構,確實不會有什么問題,如果是復雜的數(shù)據(jù)結構可能就會有問題了(使用AtomicReference可以把C A S使用在對象上),以鏈表數(shù)據(jù)結構為例,兩個線程通過C A S去刪除頭節(jié)點,假設現(xiàn)在鏈表有A->B節(jié)點
線程1刪除A節(jié)點,B節(jié)點成為頭節(jié)點,正要執(zhí)行C A S(A,A,B)時,時間片用完,切換到線程2
線程2刪除A、B節(jié)點
線程2加入C、A節(jié)點,鏈表節(jié)點變成A->C
線程1重新獲取時間片,執(zhí)行C A S(A,A,B)
丟失C節(jié)點
要解決A B A問題也非常簡單,只要追加版本號即可,每次改變時加1,即A —> B —> A,變成1A —> 2B —> 3A,在Java中提供了AtomicStampedRdference可以實現(xiàn)這個方案(面試只要問了C A S,就一定會問ABA,這塊一定要搞明白)。
Java 任務調度
版權聲明:本文內容由網(wǎng)絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內刪除侵權內容。
版權聲明:本文內容由網(wǎng)絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內刪除侵權內容。