一篇文章带你解析,乐观锁与悲观锁的优缺点
樂觀鎖與悲觀鎖
概述
樂觀鎖
總是假設最好的情況,每次去讀數據的時候都認為別人不會修改,所以不會上鎖, 但是在更新的時候會判斷一下在此期間有沒有其他線程更新該數據, 可以使用版本號機制和CAS算法實現。 樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似于write_condition機制,其實都是提供的樂觀鎖。 在Java中java.util.concurrent.atomic包下面的原子變量類就是基于CAS實現的樂觀鎖。
悲觀鎖
總是假設最壞的情況,每次去讀數據的時候都認為別人會修改,所以每次在讀數據的時候都會上鎖, 這樣別人想讀取數據就會阻塞直到它獲取鎖 (共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程)。 傳統的關系型數據庫里邊就用到了很多悲觀鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。 Java中synchronized和ReentrantLock等獨占鎖就是悲觀鎖思想的實現。
使用場景
樂觀鎖適用于寫比較少的情況下(多讀場景),即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。
悲觀鎖適用于讀比較少的情況下(多寫場景),如果是多寫的情況,一般會經常產生沖突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
樂觀鎖好比生活中樂觀的人總是想著事情往好的方向發展,悲觀鎖好比生活中悲觀的人總是想著事情往壞的方向發展。 這兩種人各有優缺點,不能不以場景而定說一種人好于另外一種人。
樂觀鎖常見的兩種實現方式
版本控制
一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version++即可。 當線程A要更新數據值時,在讀取數據的同時也會讀取version值, 在提交更新時,若剛才讀取到的version值為當前數據庫中的version值相等時才更新, 否則重試更新操作,直到更新成功。
舉個例子:
假設數據庫中帳戶信息表中有一個 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 的操作結果的可能。
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。
轉載于:https://juejin.im/post/5cdfa98c51882525f52cf6f9
總結
以上是生活随笔為你收集整理的一篇文章带你解析,乐观锁与悲观锁的优缺点的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis【3】其他部分~
- 下一篇: 几道MySQL索引相关的重点面试题