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