目錄
- 🌴 樂觀鎖 vs 悲觀鎖
- 🎍重量級鎖 vs 輕量級鎖
- 🍀自旋鎖(Spin Lock)
- 🎋公平鎖 vs ?公平鎖
- 🌳可重?鎖 vs 不可重?鎖
- 🎄讀寫鎖
- ?相關面試題
常?的鎖策略
注意: 接下來講解的鎖策略不僅僅是局限于 Java . 任何和 “鎖” 相關的話題, 都可能會涉及到以下內容.
這些特性主要是給鎖的實現者來參考的.
普通的程序猿也需要了解?些, 對于合理的使?鎖也是有很?幫助的.
🌴 樂觀鎖 vs 悲觀鎖
悲觀鎖:
總是假設最壞的情況,每次去拿數據的時候都認為別?會修改,所以每次在拿數據的時候都會上鎖,
這樣別?想拿這個數據就會阻塞直到它拿到鎖。
樂觀鎖:
假設數據?般情況下不會產?并發沖突,所以在數據進?提交更新的時候,才會正式對數據是否產?
并發沖突進?檢測,如果發現并發沖突了,則讓返回??錯誤的信息,讓??決定如何去做。
樂觀鎖的一個重要功能就是要檢測出數據是否發生訪問沖突.
那我們具體是怎么檢測的呢?這里我們我們可以引入一個 “版本號” 來解決.
那什么是版本號呢?請看下面的例子:
假設我們需要多線程修改 “用戶賬戶余額”.
設當前余額為 100. 引入一個版本號 version, 初始值為 1. 并且我們規定 "提交版本必須大于記錄當前版本”才能執行更新余額
接下來我們進行以下操作:
第一步:線程 A 此時準備將其讀出( version=1, balance=100 ),線程 B 也讀入此信息( version=1,balance=100 )
第二步:線程 A 操作的過程中并從其帳戶余額中扣除 50( 100-50 ),線程 B 從其帳戶余額中扣除 20( 100-20 );
第三步:線程 A 完成修改工作,將數據版本號加1( version=2 ),連同帳戶扣除后余額( balance=50),寫回到內存中;
第四步:線程 B 完成了操作,也將版本號加1( version=2 )試圖向內存中提交數據( balance=80),但此時比對版本發現,操作員 B 提交的數據版本號為 2 ,數據庫記錄的當前版本也為 2 ,不滿足 “提交版本必須大于記錄當前版本才能執行更新“ 的樂觀鎖策略。就認為這次操作失敗.
在Java中,Synchronized 初始使?樂觀鎖策略. 當發現鎖競爭?較頻繁的時候, 就會?動切換成悲觀鎖策略.
🎍重量級鎖 vs 輕量級鎖
鎖的核?特性 “原?性”, 這樣的機制追根溯源是 CPU 這樣的硬件設備提供的.
? CPU 提供了 “原?操作指令”.
? 操作系統基于 CPU 的原?指令, 實現了 mutex 互斥鎖.
? JVM 基于操作系統提供的互斥鎖, 實現了 synchronized 和 ReentrantLock 等關鍵字和類.
注意, synchronized 并不僅僅是對 mutex 進?封裝, 在 synchronized 內部還做了很多其
他的?作
重量級鎖: 加鎖機制重度依賴了 OS 提供了 mutex
? ?量的內核態??態切換
? 很容易引發線程的調度
這兩個操作, 成本?較?. ?旦涉及到??態和內核態的切換, 就意味著 “滄海桑?”.
輕量級鎖: 加鎖機制盡可能不使? mutex, ?是盡量在??態代碼完成. 實在搞不定了, 再使? mutex.
? 少量的內核態??態切換.
? 不太容易引發線程調度.
什么是用戶態什么是內核態?
理解??態 vs 內核態 想象去銀?辦業務. 在窗?外, ??做, 這是??態. ??態的時間成本是?較可控的. 在窗?內, ?作?員做,
這是內核態. 內核態的時間成本是不太可控的. 如果辦業務的時候反復和?作?員溝通, 還需要重新排隊, 這時效率是很低的.
synchronized 開始是?個輕量級鎖. 如果鎖沖突比較嚴重, 就會變成重量級鎖.
🍀自旋鎖(Spin Lock)
按之前的?式,線程在搶鎖失敗后進?阻塞狀態,放棄 CPU,需要過很久才能再次被調度.
但實際上, ?部分情況下,雖然當前搶鎖失敗,但過不了很久,鎖就會被釋放。沒必要就放棄 CPU. 這個時候就可以使??旋鎖來處理這樣的問題.
?旋鎖偽代碼:
while (搶鎖(lock) == 失敗) {}
如果獲取鎖失敗, ?即再嘗試獲取鎖, ?限循環, 直到獲取到鎖為?. 第?次獲取鎖失敗, 第?次的嘗試會在極短的時間內到來.
?旦鎖被其他線程釋放, 就能第?時間獲取到鎖
理解?旋鎖 vs 掛起等待鎖
想象?下, 去追求?個?神. 當男?向?神表?后, ?神說: 你是個好?, 但是我有男朋友了~~
掛起等待鎖: 陷?沉淪不能?拔… 過了很久很久之后, 突然?神發來消息, “咱倆要不試試?” (注意, 這
個很?的時間間隔?, ?神可能已經換了好?個男票了).
?旋鎖: 死?賴臉堅韌不拔. 仍然每天持續的和?神說早安晚安. ?旦?神和上?任分?, 那么就能?刻
抓住機會上位.
==?旋鎖是?種典型的 輕量級鎖 的實現?式.
? 優點: 沒有放棄 CPU, 不涉及線程阻塞和調度, ?旦鎖被釋放, 就能第?時間獲取到鎖.
? 缺點: 如果鎖被其他線程持有的時間?較久, 那么就會持續的消耗 CPU 資源. (?掛起等待的時候是不消耗 CPU 的).
synchronized 中的輕量級鎖策略?概率就是通過?旋鎖的?式實現的.
🎋公平鎖 vs ?公平鎖
假設三個線程 A, B, C. A 先嘗試獲取鎖, 獲取成功. 然后 B 再嘗試獲取鎖, 獲取失敗, 阻塞等待; 然后 C
也嘗試獲取鎖, C 也獲取失敗, 也阻塞等待.
當線程 A 釋放鎖的時候, 會發?啥呢?
公平鎖: 遵守 “先來后到”. B ? C 先來的. 當 A 釋放鎖的之后, B 就能先于 C 獲取到鎖.
?公平鎖: 不遵守 “先來后到”. B 和 C 都有可能獲取到鎖.
這就好??群男?追同?個?神. 當?神和前任分?之后, 先來追?神的男?上位, 這就是公平鎖; 如果
是?神不按先后順序挑?個??看的順眼的, 就是?公平鎖.
注意:
? 操作系統內部的線程調度就可以視為是隨機的. 如果不做任何額外的限制, 鎖就是?公平鎖. 如果要
想實現公平鎖, 就需要依賴額外的數據結構, 來記錄線程們的先后順序.
? 公平鎖和?公平鎖沒有好壞之分, 關鍵還是看適?場景.
synchronized 是?公平鎖.
🌳可重?鎖 vs 不可重?鎖
可重?鎖的字?意思是“可以重新進?的鎖”,即允許同?個線程多次獲取同?把鎖。
?如?個遞歸函數?有加鎖操作,遞歸過程中這個鎖會阻塞??嗎?如果不會,那么這個鎖就是可重?鎖(因為這個原因可重?鎖也叫做遞歸鎖)。
Java?只要以Reentrant開頭命名的鎖都是可重?鎖,?且JDK提供的所有現成的Lock實現類,包括synchronized關鍵字鎖都是可重?的。
? Linux 系統提供的 mutex 是不可重?鎖.
理解 “把??鎖死”
?個線程沒有釋放鎖, 然后?嘗試再次加鎖.
1 // 第?次加鎖, 加鎖成功
2 lock();
3 // 第?次加鎖, 鎖已經被占?, 阻塞等待.
4 lock();
按照之前對于鎖的設定, 第?次加鎖的時候, 就會阻塞等待. 直到第?次的鎖被釋放, 才能獲取到第?個
鎖. 但是釋放第?個鎖也是由該線程來完成, 結果這個線程已經躺平了, 啥都不想?了, 也就?法進?解
鎖操作. 這時候就會 死鎖.
這樣的鎖稱為 不可重?鎖.
最后,要記得
synchronized 是可重入鎖
🎄讀寫鎖
多線程之間,數據的讀取?之間不會產?線程安全問題,但數據的寫??互相之間以及和讀者之間都
需要進?互斥。如果兩種場景下都?同?個鎖,就會產?極?的性能損耗。所以讀寫鎖因此?產?。
讀寫鎖(readers-writer lock),看英?可以顧名思義,在執?加鎖操作時需要額外表明讀寫意圖,復數讀者之間并不互斥,?寫者則要求與任何?互斥。
?個線程對于數據的訪問, 主要存在兩種操作: 讀數據 和 寫數據.
? 兩個線程都只是讀?個數據, 此時并沒有線程安全問題. 直接并發的讀取即可.
? 兩個線程都要寫?個數據, 有線程安全問題.
? ?個線程讀另外?個線程寫, 也有線程安全問題.
讀寫鎖就是把讀操作和寫操作區分對待. Java 標準庫提供了 ReentrantReadWriteLock 類, 實現
了讀寫鎖
- ReentrantReadWriteLock.ReadLock 類表??個讀鎖. 這個對象提供了 lock / unlock ?法
進?加鎖解鎖. - ReentrantReadWriteLock.WriteLock 類表??個寫鎖. 這個對象也提供了 lock / unlock
?法進?加鎖解鎖
其中,
- 讀加鎖和讀加鎖之間, 不互斥.
- 寫加鎖和寫加鎖之間, 互斥.
- 讀加鎖和寫加鎖之間, 互斥.
注意, 只要是涉及到 “互斥”, 就會產?線程的掛起等待. ?旦線程掛起, 再次被喚醒就不知道隔了多久
了.
因此盡可能減少 “互斥” 的機會, 就是提?效率的重要途徑
讀寫鎖特別適合于 “頻繁讀, 不頻繁寫” 的場景中. (這樣的場景其實也是?常?泛存在的).
?如學校的教務系統.
每節課?師都要使?教務系統點名, 點名就需要查看班級的同學列表(讀操作). 這個操作可能要每周執
?好?次.
?什么時候修改同學列表呢(寫操作)? 就新同學加?的時候. 可能?個?都不必改?次.
再?如, 同學們使?教務系統查看作業(讀操作), ?個班級的同學很多, 讀操作?天就要進???次上
百次.
但是這?節課的作業, ?師只是布置了?次(寫操作)
Synchronized 不是讀寫鎖.、
?相關面試題
- 你是怎么理解樂觀鎖和悲觀鎖的,具體怎么實現呢?
悲觀鎖認為多個線程訪問同?個共享變量沖突的概率較?, 會在每次訪問共享變量之前都去真正加鎖.
樂觀鎖認為多個線程訪問同?個共享變量沖突的概率不?. 并不會真的加鎖, ?是直接嘗試訪問數據.
在訪問的同時識別當前的數據是否出現訪問沖突.
悲觀鎖的實現就是先加鎖(?如借助操作系統提供的 mutex), 獲取到鎖再操作數據. 獲取不到鎖就等待.
樂觀鎖的實現可以引??個版本號. 借助版本號識別出當前的數據訪問是否沖突. (實現細節參考上?
的圖).
2.介紹下讀寫鎖?
讀寫鎖就是把讀操作和寫操作分別進?加鎖.
讀鎖和讀鎖之間不互斥
寫鎖和寫鎖之間互斥.
寫鎖和讀鎖之間互斥.
讀寫鎖最主要?在 “頻繁讀, 不頻繁寫” 的場景中.、
3.什么是?旋鎖,為什么要使??旋鎖策略呢,缺點是什么?
如果獲取鎖失敗, ?即再嘗試獲取鎖, ?限循環, 直到獲取到鎖為?. 第?次獲取鎖失敗, 第?次的嘗試
會在極短的時間內到來. ?旦鎖被其他線程釋放, 就能第?時間獲取到鎖.
相?于掛起等待鎖,
優點: 沒有放棄 CPU 資源, ?旦鎖被釋放就能第?時間獲取到鎖, 更?效. 在鎖持有時間?較短的場景
下?常有?.
缺點: 如果鎖的持有時間較?, 就會浪費 CPU 資源.
4.synchronized 是可重?鎖么?
是可重?鎖.
可重?鎖指的就是連續兩次加鎖不會導致死鎖.
實現的?式是在鎖中記錄該鎖持有的線程?份, 以及?個計數器(記錄加鎖次數). 如果發現當前加鎖的
線程就是持有鎖的線程, 則直接計數?增.