一篇文章帶你解析,樂觀鎖與悲觀鎖的優缺點

樂觀鎖與悲觀鎖

概述

樂觀鎖

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

悲觀鎖

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

使用場景

  • 樂觀鎖適用於寫比較少的狀況下(多讀場景),即衝突真的不多發生的時候,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。數據庫

  • 悲觀鎖適用於讀比較少的狀況下(多寫場景),若是是多寫的狀況,通常會常常產生衝突,這就會致使上層應用會不斷的進行retry,這樣反卻是下降了性能,因此通常多寫的場景下用悲觀鎖就比較合適。bash

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

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

版本控制

通常是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version++便可。 當線程A要更新數據值時,在讀取數據的同時也會讀取version值, 在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新, 不然重試更新操做,直到更新成功。性能

舉個例子:atom

假設數據庫中賬戶信息表中有一個 version 字段,而且 version=1;而當前賬戶餘額字段(balance)爲 $100 。

操做員 A 此時將其讀出 (version=1),並從其賬戶餘額中扣除 $50($100-$50)。

操做員 A 操做的同事,操做員B 也讀入此用戶信息(version=1),並從其賬戶餘額中扣除 $20($100-$20)。

操做員 A 完成了修改工做,version++(version=2),連同賬戶扣除後餘額(balance=$50),提交至數據庫更新,
此時因爲提交數據版本大於數據庫記錄當前版本,數據被更新,數據庫記錄 version 更新爲 2 。

操做員 B 完成了操做,也將版本號version++(version=2)試圖向數據庫提交數據(balance=$80),
但此時比對數據庫記錄版本時發現,操做員 B 提交的數據版本號爲 2 ,數據庫記錄當前版本也爲 2 ,
不知足**提交版本必須大於記錄當前版本才能執行更新**的樂觀鎖策略,所以,操做員 B 的提交被駁回。複製代碼

避免了操做員 B 用基於 version=1 的舊數據修改的結果覆蓋操做員A 的操做結果的可能。spa

CAS算法

硬件支持的原子性操做最典型的是:比較並交換(Compare-and-Swap,CAS)。 CAS 指令須要有 3 個操做數,分別是內存地址 V、舊的預期值 A 和新值 B。 當執行操做時,只有當 V 的值等於 A,纔將 V 的值更新爲 B。線程

//著名的CAS
//var1是比較值所屬的對象,var2須要比較的值(但實際是使用地址偏移量來實現的),
//若是var1對象中偏移量爲var2處的值等於var4,那麼將該處的值設置爲var5並返回true,若是不等於var4則返回false。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);複製代碼

樂觀鎖的缺點

1.ABA問題版本控制

若是一個變量初次讀取的時候是 A 值,它的值被改爲了 B,後來又被改回爲 A,那 CAS 操做就會誤認爲它歷來沒有被改變過。

J.U.C 包提供了一個帶有標記的原子引用類 AtomicStampedReference 來解決這個問題, 它能夠經過控制變量值的版原本保證 CAS 的正確性。 大部分狀況下 ABA 問題不會影響程序併發的正確性, 若是須要解決 ABA 問題,改用傳統的互斥同步可能會比原子類更高效。

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的使用情景

  • 對於資源競爭較少(線程衝突較輕)的狀況, 使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操做額外浪費消耗cpu資源; 而CAS基於硬件實現,不須要進入內核,不須要切換線程,操做自旋概率較少,所以能夠得到更高的性能。

  • 對於資源競爭嚴重(線程衝突嚴重)的狀況,CAS自旋的機率會比較大, 從而浪費更多的CPU資源,效率低於synchronized。

相關文章
相關標籤/搜索