面試必備之樂觀鎖與悲觀鎖(程式設計師必看)

只愛宅zmy發表於2020-10-26
何謂悲觀鎖與樂觀鎖
樂觀鎖對應於生活中樂觀的人總是想著事情往好的方向發展,悲觀鎖對應於生 活中悲觀的人總是想著事情往壞的方向發展。這兩種人各有優缺點,不能不以 場景而定說一種人好於另外一種人。

悲觀鎖

總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會.上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖(共享資 源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒)。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖 等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java 中synchronized 和 Reentrantlock等獨佔鎖就是悲觀鎖思想的實現。

樂觀鎖

總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以 使用版本號機制和CAS演算法實現。樂觀鎖適用於多讀的應用型別,這樣可以提 高吞吐量,像資料庫提供的類似於write_ condition機制,其實都是提供的樂觀鎖。在Java中java.util. concurrent. atomic包下面的原子變數類就是使用了 樂觀鎖的一種實現方 式CAS實現的。

兩種鎖的使用場景

從上面對兩種鎖的介紹,我們知道兩種鎖各有優缺點,不可認為一種好於另一 種,像樂觀鎖適用於寫比較少的情況下(多讀場景),即衝突真的很少發生的 時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的 情況,一般會經常產生衝突,這就會導致上層應用會不斷的進行retry,這樣反 倒是降低了效能,所以一般多寫的場景下用悲觀鎖就比較合適。

樂觀鎖常見的兩種實現方式

樂觀鎖一般會使用版本號機制或CAS演算法實現。

1.版本號機制

一般是在資料表中加上一個資料版本號version欄位,表示資料被修改的次 數,當資料被修改時,version 值會加一。當執行緒A要更新資料值時,在讀取數 據的同時也會讀取version值,在提交更新時,若剛才讀取到的version值為當 前資料庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
**舉一個簡單的例子:**假 設資料庫中帳戶資訊表中有一個version 欄位,當前值 為1 ;而當前帳戶餘額欄位( balance )為$100。
1.操作員A此時將其讀出( version=1 ),並從其帳戶餘額中扣除$50 ( $100-$50 )。
2. 在操作員A操作的過程中,操作員B也讀入此使用者資訊( version=1 ),並從其帳戶餘額中扣除$20 ( $100-$20 )。
3. 操作員A完成了修改工作,將資料版本號加一( version=2 ),連同帳戶扣除後餘額( balance=$50 ),提交至資料庫更新,此時由於提交資料版本大於資料庫記錄當前版本,資料被更新,資料庫記錄 version更新為2。
4.操作員B完成了操作,也將版本號加一( version=2 )試圖向資料庫 提交資料( balance=$80 ),但此時比對資料庫記錄版本時發現,操 作員B提交的資料版本號為2,資料庫記錄當前版本也為2,不滿 足”提交版本必須大於記錄當前版本才能執行更新”的樂觀鎖策略, 因此,操作員B的提交被駁回。
這樣,就避免了操作員B用基於version=1的舊資料修改的結果覆蓋操作員 A的操作結果的可能。

2.CAS演算法.

即compare and swap (比較與交換),是一種有名的無鎖演算法。無鎖程式設計, 即不使用鎖的情況下實現多執行緒之間的變數同步,也就是在沒有執行緒被阻塞的 情況下實現變數的同步,所以也叫非阻塞同步( Non-blocking Synchronization)。CAS演算法涉及到三個運算元
●需要讀寫的記憶體值 V
●進行比較的值A
●擬寫入的新值B
當且僅當V的值等於A時,CAS 透過原子方式用新值B來更新V的值,否則 不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操 作,即不斷的重試。

樂觀鎖的缺點

ABA問題是樂觀鎖一個常見的問題

1. ABA問題

如果一個變數V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然 是A值,那我們就能說明它的值沒有被其他執行緒修改過了嗎?很明顯是不能 的,因為在這段時間它的值可能被改為其他值,然後又改回A,那CAS操作就 會誤認為它從來沒有被修改過。這個問題被稱為CAS操作的**"ABA"問題。 **
JDK 1.5以後的AtomicStampedReference類就提供了此種能力,其中的 compareAndSet方法就是首先檢查當前引用是否等於預期引用,並且當前標誌是 否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。

2.迴圈時間長開銷大

**自旋CAS (也就是不成功就一直迴圈執行直到成功)如果長時間不成功,會給 CPU帶來非常大的執行開銷。**如果JVM能支援處理器提供的pause指令那麼 效率會有一定的提升,pause 指令有兩個作用,第一它可以延遲流水線執行指 令(de-pipeline) ,使CPU不會消耗過多的執行資源,延遲的時間取決於具體 實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時 候因記憶體順序衝突( memory order violation)而引起CPU流水線被清空 (CPU pipeline flush),從而提高CPU的執行效率。

3.只能保證一個共享變t的原子操作

CAS只對單個共享變數有效,當操作涉及跨多個共享變數時CAS無效。但是 從JDK 1.5開始,提供了AtomicReference 類來保證引用物件之間的原子性,你 可以把多個變數放在一個對 象裡來進行CAS操作.所以我們可以使用鎖或者利 用AtomicReference類把多個共享變數合併成一個共享變數來操作。

CAS與synchronized的使用情景

簡單的來說CAS適用於寫比較少的情況下(多讀場景,衝突一般較少), synchronized適用於寫比較多的情況下(多寫場景,衝突一般較多)
1.對於資源競爭較少(執行緒衝突較輕)的情況,使用synchronized同步鎖 進行執行緒阻塞和喚醒切換以及使用者態核心態間的切換操作額外浪費消耗 cpu資源:而CAS基於硬體實現,不需要進入核心,不需要切換執行緒, 操作自旋機率較少,因此可以獲得更高的效能。
2. 對於資源競爭嚴重(執行緒衝突嚴重)的情況,CAS自旋的機率會比較 大,從而浪費更多的CPU資源,效率低於synchronized.

補充:

Java 併發程式設計這個領域中synchronized關鍵字一直 都是元老級的角色,很久之前很多人都會稱它為**“重量級鎖**”。但是,在JavaSE 1.6之後進行了 主要包括為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖和輕t級 鎖以及其它各種最佳化之後變得在某些情況下並不是那麼重了。synchronized 的 底層實現主要依靠Lock-Free的佇列,基本思路是自旋後阻塞,競爭切換後繼 續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐t。線上程衝突較少的情況 下,可以獲得和CAS類似的效能;而執行緒衝突嚴重的情況下,效能遠高於 CAS。

-end-

作者:圖靈程式設計師
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983372/viewspace-2729722/,如需轉載,請註明出處,否則將追究法律責任。

相關文章