讀寫(xiě)鎖 + HashMap 超級(jí)組合,真心推薦!
這篇文章,我們深入聊聊:讀寫(xiě)鎖如何保證 HashMap 成為一個(gè)線程安全的容器。
圖片
1.編程范式例子
圖片
上圖展示了使用讀寫(xiě)鎖對(duì) HashMap 進(jìn)行操作的編程范式,核心要點(diǎn):
- 單獨(dú)的類用于封裝對(duì) HashMap 的讀、寫(xiě)操作;
- 讀操作方法內(nèi)部,先獲取讀鎖,讀取數(shù)據(jù)之后,釋放讀鎖;
- 寫(xiě)操作方法內(nèi)部,先獲取寫(xiě)鎖,寫(xiě)入成功之后,釋放寫(xiě)鎖。
很多同學(xué)問(wèn)我:”勇哥,假如讀鎖申請(qǐng)成功后,寫(xiě)鎖會(huì)阻塞嗎 ?“ 或者 ”寫(xiě)鎖申請(qǐng)成功后,讀鎖會(huì)被阻塞嗎?“ 。
答案是肯定的,讀寫(xiě)必然互斥 。
筆者分別寫(xiě)兩個(gè)簡(jiǎn)單的例子,并展示堆棧圖,大家就可以一目了然。
2.讀鎖申請(qǐng)成功后,寫(xiě)鎖會(huì)被阻塞
我們將 ReadWriteLockCache 的讀操作修改如下:
圖片
然后編寫(xiě) main 方法:
圖片
main 方法中,我們先后啟動(dòng)讀線程、寫(xiě)線程 。
圖片
我們通過(guò) IDEA 打印堆棧日志,發(fā)現(xiàn):讀線程先獲取讀鎖,然后休眠 10 秒,這樣讀鎖就不會(huì)釋放,后面寫(xiě)線程嘗試獲取寫(xiě)鎖時(shí),寫(xiě)線程阻塞了。
3.寫(xiě)鎖申請(qǐng)成功后,讀鎖會(huì)被阻塞
我們將 ReadWriteLockCache 的讀操作代碼還原,然后將寫(xiě)操作修改如下:
圖片
然后編寫(xiě) Main 方法:
圖片
main 方法中,我們先后啟動(dòng)寫(xiě)線程、讀線程 。
圖片
我們通過(guò) IDEA 打印堆棧日志,發(fā)現(xiàn):寫(xiě)線程先獲取寫(xiě)鎖,然后休眠 10 秒,這樣寫(xiě)鎖就不會(huì)釋放,后面讀線程嘗試獲取讀鎖時(shí),線程阻塞了。
4.使用 ConcurrentHashMap 是不是更簡(jiǎn)單點(diǎn)
有的同學(xué)會(huì)問(wèn):使用 ConcurrentHashMap 是不是更簡(jiǎn)單點(diǎn)嗎 ?
我們分兩個(gè)層面來(lái)說(shuō)明:
1)讀寫(xiě)鎖 + 多個(gè) HashMap
讀寫(xiě)鎖可以操作多個(gè) HashMap ,每次寫(xiě)操作需要同時(shí)變更多個(gè) HashMap ,為了保證其一致性,故需要加鎖,ConcurrentHashMap 并發(fā)容器在多線程環(huán)境下的線程安全也只是針對(duì)其自身,故從這個(gè)維度,選用讀寫(xiě)鎖是必然的選擇 。
我們舉 RocketMQ NameServer 的經(jīng)典案例:
Broker 啟動(dòng)之后會(huì)向所有 NameServer 定期(每 30s)發(fā)送心跳包(路由信息),NameServer 會(huì)定期掃描 Broker 存活列表,如果超過(guò) 120s 沒(méi)有心跳則移除此 Broker 相關(guān)信息,代表下線。
那么 NameServer 如何保存路由信息呢?
圖片
路由信息通過(guò)幾個(gè) HashMap 來(lái)保存,當(dāng) Broker 向 Nameserver 發(fā)送心跳包(路由信息),Nameserver 需要對(duì) HashMap 進(jìn)行數(shù)據(jù)更新,但我們都知道 HashMap 并不是線程安全的,高并發(fā)場(chǎng)景下,容易出現(xiàn) CPU 100% 問(wèn)題,所以更新 HashMap 時(shí)需要加鎖,RocketMQ 使用了 JDK 的讀寫(xiě)鎖 ReentrantReadWriteLock 。
- 更新路由信息,操作寫(xiě)鎖
圖片
- 查詢主題信息,操作讀鎖
圖片
2)讀寫(xiě)鎖 + 1 個(gè) HashMap
假如我們僅僅使用讀寫(xiě)鎖操作 1 個(gè) HashMap ,那么我們需要分析下 ConcurrentHashMap 的原理。
- JDK 8 之前
圖片
從圖中我們可以看出, ConcurrentHashMap 內(nèi)部進(jìn)行了 Segment 分段,Segment 繼承了 ReentrantLock,可以理解為一把鎖,各個(gè) Segment 之間都是相互獨(dú)立上鎖的,互不影響。
同一個(gè) Segment 的讀寫(xiě)都需要加鎖,即落在同一個(gè) Segment 中的讀、寫(xiě)操作是串行的,其讀的并發(fā)性低于讀寫(xiě)鎖 + HashMap 的,
因此在 JDK 1.8 之前,ConcurrentHashMap 是落后于讀寫(xiě)鎖 + HashMap 的結(jié)構(gòu)的。
- JDK 1.8 及其后續(xù)版本
圖片
JDK 1.8 對(duì) ConcurrentHashMap 代碼進(jìn)行了大幅優(yōu)化,存儲(chǔ)結(jié)構(gòu)與 HashMap 非常類似,同時(shí)引入了 CAS 機(jī)制(輕量級(jí)) 來(lái)解決并發(fā)更新。
因此,相比讀寫(xiě)鎖操作 1 個(gè) HashMap, 使用 ConcurrentHashMap 更具性能優(yōu)勢(shì)。
5.總結(jié)
這篇文章,我們深入剖析:讀寫(xiě)鎖如何保證 HashMap 成為一個(gè)線程安全的容器。
1)讀寫(xiě)鎖編程范式
- 單獨(dú)的類用于封裝對(duì) HashMap 的讀、寫(xiě)操作;
- 讀操作方法內(nèi)部,先獲取讀鎖,讀取數(shù)據(jù)之后,釋放讀鎖;
- 寫(xiě)操作方法內(nèi)部,先獲取寫(xiě)鎖,寫(xiě)入成功之后,釋放寫(xiě)鎖。
2)兩個(gè)實(shí)驗(yàn)例子
- 讀鎖申請(qǐng)成功后,寫(xiě)線程申請(qǐng)寫(xiě)鎖會(huì)阻塞
- 寫(xiě)鎖申請(qǐng)成功后,讀線程申請(qǐng)讀鎖會(huì)阻塞
我們用兩個(gè)實(shí)驗(yàn)突出了讀寫(xiě)鎖的特性:讀讀不互斥,讀寫(xiě)互斥,寫(xiě)寫(xiě)互斥 。
3)使用 ConcurrentHashMap 是不是更簡(jiǎn)單點(diǎn)
- 假如需要操作 多個(gè) HashMap ,那么讀寫(xiě)鎖更加有優(yōu)勢(shì) ;
- 假如僅僅操作 1個(gè) HashMap , 建議使用 JDK 1.8 ConcurrentHashMap ,性能會(huì)更好。