專欄特色:結合10余年的作業經驗,在實踐中提煉總結高并發經驗,將理論落到實處,不僅助力面試,更是真正提高技能,
專欄目錄:
- 《重學Java高并發》Sempahore的使用場景與常見誤區
- 《重學Java高并發》手寫一個生產者消費者執行緒模型
正確理解鎖是深入理解Java并發的重中之重,
接下來和筆者一步一步進入"Java的鎖世界"中來吧,本文將循序漸進的介紹鎖的相關知識,從簡單到難,從概念到實踐思路,
1、鎖的種類
首先以一個非常常見的生活場景舉例,例如一個三口之家居住在一個二房一廳的房子里,只有一個衛生間,早上一起床,大家是不是都有搶衛生間,這里就會發生一個有意思的事情了,一人在如廁,其他人排隊等待的場景,

這個場景下有如下幾個關鍵的特征:
- 獨占
“廁所“作為一個資源,在任意時刻只能被一個人占用,為了實作該效果,使用資源之前,需要先獲得與該資源關聯的鎖 - 當多個執行緒都需要訪問該資源時,必須先獲得鎖,而且在同一時刻有且只會有一個執行緒獲得鎖,那沒有獲得鎖的執行緒就需要排隊等待,是一直等,還是等得不耐煩時就放棄?
- 當有多人排隊時,一個執行緒將鎖釋放后,交給誰?什么樣的策略?
上面是最常見的鎖應用場景,有一個非常響亮的名稱:互斥鎖、排它鎖,
1.1 互斥鎖
在java領域中,實作互斥鎖通常有兩種方式:
- synchronized
- ReentrantLock
接下來對比兩者的不同點,從而來了解互斥鎖的基本語意,
-
可重入性
所謂的可重入性一個執行緒獲取鎖后,沒有釋放之前,繼續申請,其偽代碼如下所示:

synchronized與ReentrantLock都支持可重入性, -
鎖只能被鎖的擁有者釋放
大家如何基于redis實作分布式鎖時,要特別注意這個特質, -
申請鎖時是否支持超時取消
申請鎖的時候,ReentrantLock能支持設定超時時間,即呼叫申請鎖時如果在指定時間內未獲取鎖,支持自動停止阻塞跳出,而synchronized不支持, -
申請鎖時是否支持被中斷
ReentrantLock可以通過呼叫lockInterruptibly方法,可以支持執行緒中斷,即停止繼續申請鎖,同樣synchronized不支持, -
是否支持公平鎖/非公平鎖
所謂的公平鎖,是指當擁有鎖的執行緒釋放鎖后,鎖的下一個獲取者就是鎖等待佇列中的第一個元素,而非公平鎖并沒有這個限制,ReentrantLock支持,而synchronized不支持,
1.2 共享鎖
與互斥鎖相對應的是共享鎖,所謂的共享鎖是同一時間可以被多個執行緒共同申請,一個非常經典的使用場景就是讀寫鎖,
例如在一個快取場景,在一個商品系統中,為了提供對商品的訪問性能,通常會引入一個快取區(Map)來快取商品的資料,快取資料對查詢請求(讀請求)是可以并行執行的,即多個執行緒同時查詢快取區的資料,這個是一個非常安全的操作,但不允許多個執行緒對快取區進行修改,這里共享鎖的意義就發揮出來了,
既然多個執行緒對快取區可以同時進行讀操作,那為什么還要加共享鎖呢?主要的目的是避免寫操作與讀操作同時進行,
只要當前有讀操作在進行,寫操作就需要排隊,請看如下示例圖:

如上圖所示:例如 執行緒T1,T2,T3連續申請共享鎖,然后T4申請寫鎖,再T5申請讀鎖,那各個執行緒的并發執行情況如下所示:
- 執行緒 T1、T2、T3 將并發執行
- T4由于是申請的寫鎖,必須等 T1、T2、T3釋放鎖后,才能執行,
- T5雖然申請是共享鎖,但由于T4持有寫鎖,故T5也需要阻塞,直至T4釋放鎖,
在Java等世界中按鎖的排斥性來分基本就包含排它鎖與共享鎖,其他讀寫鎖、間隙鎖等是以鎖的粒度這個緯度進行細分,
2、鎖的實作原理
在了解了鎖的基本語意義之后,我們有必要來闡述一下鎖的實作原理,
從某種意義上來說,鎖的實作原理就是兩個佇列:同步阻塞佇列、條件等待佇列,
2.1 阻塞佇列
阻塞佇列的作用說明如下圖所示:

上面使用來synchronized,其傳入的是一個鎖物件,如果此時有5個執行緒同時去執行這段代碼,由于鎖的互斥性,同一時間只有一個執行緒能獲得鎖,其他執行緒需要排隊等待,故需要引入一個佇列來存盤在這些排隊的執行緒,所以synchronized的實作機制中,會在鎖物件中開辟一個佇列,用來存盤等待獲取當前鎖的執行緒,
2.2 條件等待佇列
Object物件中有一對特殊的方法:wait()/notify()/notifyAll(),大家在前文中應該看到消費者/生產者中示例中,使用過wait,notify方法,示例代碼如下:

wait方法必須在synchronized中呼叫,并且通常是執行緒呼叫鎖物件的wait方法,表示當前繼續往下執行的條件不足,當前執行緒需要等待,故需要為鎖物件再維護一個個佇列,用來存盤等待的執行緒,俗稱條件等待佇列,
當其他執行緒呼叫鎖物件的notify方法或notifyAll方法,會喚醒等待佇列中的執行緒,
溫馨提示:上述還有幾個關鍵點:
- Object.wait方法,會使當前執行緒進入等待狀態,并且釋放鎖,
- 通常條件等待會使用while陳述句,避免條件不滿足時被誤喚醒,故使用while對條件進行再一次的判斷,
- 當被喚醒后,并不立即去執行while條件判斷,而是需要重新去申請鎖,即可能會進入到阻塞佇列,
3、鎖的優化思路
我相信作為一個程式員,大家都對鎖很敏感,因為性能低下,但鎖肯定有其存在的原因,主要解決資料訪問的安全性,大家可能會感到驚訝,作為一款高性能的訊息中間件(RocketMQ),在訊息寫入時也使用了鎖,其代碼如下:

這是因為RocketMQ是順序寫檔案,多個請求同時申請寫一個檔案,必須排隊執行,否則會帶來邏輯例外,此時鎖是不用不行了,
對鎖的優化策略,通常基于如下原則:能不用鎖就不使用鎖,必須使用鎖則盡量保證被鎖包裹代碼的快速執行、降低鎖的粒度,
3.1 優化鎖執行時間
當然能不用鎖就不用鎖,但有些場景是必須使用鎖來保證多執行緒環境下結果的正確性,就以RocketMQ順序寫commitlog檔案為例,對同一個檔案寫入,需要記錄當前的寫入位置,然后另外一個執行緒就進行追加,故這個為寫入位置是多執行緒不安全的,故必須引入鎖,那RocketMQ作為一款高性能的訊息中間件,是如何做到訊息發送的高并發,低延遲能力低呢?
核心法寶:控制鎖的范圍,確保被鎖包含的代碼執行性能高效,接下來我們看一下RocketMQ訊息寫入的幾個重要步驟:

并不是需要將上述三個步驟都加鎖,而是只對寫記憶體這段加鎖即可,這段代碼非常高效,
3.2 優化鎖的粒度
鎖的性能優化是一個永恒的主旨,另外一個核心思路是:降低鎖的粒度,提高并發度,
接下來我們以JDK中的HashTable與ConcurrentHashMap的實作原理為例,讓大家體會一下如何降低鎖的粒度從而提高并發度,
Hashtable的性能低下是眾所周知,因為整個容器就一把鎖,因為它的get、put都是被synchronized修飾,synchronized用來修飾非static方法,其鎖物件為Hashtable是物件鎖,
并發度:同一時間只有一個執行緒能向該容器添加資料、獲取資料,
而jkd1.7及其版本,ConcurrentHashMap的內部資料結構如下圖所示:

可以看出ConcurrentHashMap的設計思路是將整個HashMap分割成多個小的HashMap,然后為每一個HashMap加鎖,從而降低鎖的粒度,從而提高并發度,
在JDK1.8及版本后,ConcurrentHashMap的存盤結構又發了很大改變,摒棄分段思想,使用來陣列 + Node ,進一步釋放讀寫的并發度,其資料結構如下圖所示:

其中,對每一個鏈表的Node節點,寫操作時會加鎖,但在查詢時候,并不會對各個Node加鎖,提高讀操作的并發度;并且會基于CAS機制實作無鎖化處理,使用volatile保證可見性,
本文并不準備去剖析ConcurrentHashMap的實作細節,后續專門從原始碼實作的角度深度剖析,敬請期待,也請持續關注我,
3.3 無鎖化設計
鎖的存在必然有其使用場景,特別是需要被鎖保護的資源眾多,即臨界區中的邏輯復雜,對其進行拆分會使代碼變的臃腫,直接使用鎖保護會清晰明了,但評估是否需要引入鎖時需要慎重,特別是一些對吞吐量有極高要求的場景,能不用鎖就不要用鎖.
無鎖化設計的基礎:CAS,比較和交換,
在Java領域也提供了對應的原子操作工具:CAS,CAS 操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B), 如果記憶體位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值 ,否則,處理器不做任何操作,CAS是CPU指令級命令,
CAS簡單使用示例如下:

正如筆者在優化信號量釋放邏輯時引入了cas確保一個SempahoreReleaseOnlyOne只會釋放一次信號量,
在JUC框架中的ArrayBlockingQueue,LinkedBlockingQueue等佇列都支持多個執行緒同時往佇列中寫入資料,但其內部都引入了鎖,
多個執行緒往佇列中寫入資料,一定要加鎖?怎么進行無鎖化設計呢?
Disruptor框架,實作多執行緒環境中真正的無鎖化設計,極大的提升并發性能,提供了多個執行緒可以同時并發安全的往同一佇列寫入資料,而不加鎖,是不是很神奇?
由于篇幅的原因,本文并不會為大家揭曉Disruptor是如何實作多個執行緒在不引入鎖的情況下對佇列進行并發操作的,興趣是最好的老師,如果大家有興趣可以先提前研究,后續將在《重學Java高并發》系列中后續文章中專門詳細剖析,
一鍵三連(關注、點贊、留言)是對我最大的鼓勵,
各位技術朋友們,我是《RocketMQ技術內幕》一書作者,CSDN2020博客之星TOP2,熱衷于中間件領域的技術分享,維護「中間件興趣圈」公眾號,旨在成體系剖析Java主流中間件,構建完備的分布式架構體系,歡迎大家大家關注我,第一時間獲得最新干貨文章,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/300291.html
標籤:其他
下一篇:C語言習題積累(正在更新)
