樂觀鎖與悲觀鎖

轉載自github

何謂悲觀鎖與樂觀鎖

樂觀鎖對應於生活中樂觀的人老是想着事情往好的方向發展,悲觀鎖對應於生活中悲觀的人老是想着事情往壞的方向發展。這兩種人各有優缺點,不能不以場景而定說一種人好於另一種人。java

悲觀鎖

老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。Java中synchronizedReentrantLock等獨佔鎖就是悲觀鎖思想的實現。git

樂觀鎖

老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。github

兩種鎖的使用場景

從上面對兩種鎖的介紹,咱們知道兩種鎖各有優缺點,不可認爲一種好於另外一種,像樂觀鎖適用於寫比較少的狀況下(多讀場景),即衝突真的不多發生的時候,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。但若是是多寫的狀況,通常會常常產生衝突,這就會致使上層應用會不斷的進行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的值,不然不會執行任何操做(比較和替換是一個原子操做)。通常狀況下是一個自旋操做,即不斷的重試。ide

關於自旋鎖,你們能夠看一下這篇文章,很是不錯:《 面試必備之深刻理解自旋鎖》

樂觀鎖的缺點

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 只能保證一個共享變量的原子操做

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以後進行了主要包括爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的 偏向鎖 和 輕量級鎖 以及其它各類優化以後變得在某些狀況下並非那麼重了。synchronized的底層實現主要依靠 Lock-Free 的隊列,基本思路是 自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但得到了高吞吐量。在線程衝突較少的狀況下,能夠得到和CAS相似的性能;而線程衝突嚴重的狀況下,性能遠高於CAS。

相關文章
相關標籤/搜索