聊聊ConcurrentHashMap
聊聊ConcurrentHashMap
一、為什么需要ConcurrentHashMap
1、HashMap 線程不安全
在多線程環境下,使用HashMap進行 put 操作的時候可能造成死循環,導致 CPU 使用率太高
為什么HashMap線程不安全?
在 put 的時候,插入元素超過了容量,就會進行rehash,這個會重新將原數組的內容重新hash到新的擴容數組中,在多線程的環境下,存在同時其他的元素也在進行 put 操作,如果 hash 值相同,可能出現同時在同一數組下用鏈表表示,造成閉環,導致在 get 時會出現死循環,所以不安全
2、HashTable 安全但是效率太低了
因為 HashTable 是利用 synchronized 來保證線程安全的,在線程競爭激烈的情況下效率將會非常低。因為在一個線程訪問同步方法的時候,其他線程只能阻塞等待。
二、ConcurrentHashMap 的 好處
實現了前面的兩個問題 :線程安全了 并且也解決了HashTable 效率低下的問題
三、怎么解決效率低下的問題
這個主要是運用了分段鎖(segment)的思想
HashTable 為什么效率低,就是因為它是多個線程競爭同一把鎖,那么如果容器里面有很多把鎖,這個問題是不是就可以解決了,這個就是ConcurrentHashMap所使用的分段鎖技術。
首先將數據分為一段一段的進程存儲,然后給每一段分別加上鎖,當一個線程占用鎖訪問其中一個段的數據的時候,其他段的數據也可以被其他線程訪問。
接下來我們就重點講講分段鎖吧
ConcurrentHashMap 是由Segment 數組結構和HashEntry數組結構組成的。Segment是一種可重入鎖ReentrantLock,在 ConcurrentHashMap 里面扮演鎖的角色,HashEntry 則是用來村塾鍵值對數據。一個 ConcurrentHashMap 里面包含一個 Segment數組,Segment的結構和HashMap類似,是一種數組+鏈表結構,一個Segment里面包含一個 HashEntry數組,每個HashEntry是一個鏈表節點構成的元素,每個Segment守護一個HashEntry數組里面的元素,當對HashEntry數組的元素進行修改時,必須首先獲得它對應的Segment鎖。可以說,ConcurrentHashMap是一個二級的哈希表。在一個總的哈希表下面還有若干個子哈希表。
四、采用分段鎖技術的好處:并發的讀寫
case1:不同Segment的并發寫入:
不同的Segment的寫入是可以并發執行的
case 2:同一個Segment 的寫
Segment的寫入是需要上鎖的,因此對同一個Segment的并發寫會被阻塞
case 3:同一個Segment 的寫-讀
同一個Segment的寫-讀是可以并發執行的
五、詳細看看 讀-寫 的過程
1、讀:Get()
- 為輸入的Key做 Hash 運算,得到 hash 值
- 通過 hash 值,定位到對應的Segment 對象
- 再次通過 hash 值,定位到 Segment 當中數組的具體位置
讀操作其實是沒有鎖的,第一次通過 hash 定位到 Segment 上,第二次通過 hash 定位到 具體元素上。因為 hashEntry 中的 value 屬性是用 volatile 修飾的,保證了可見性,所以每次獲取的都死最新值。
2、寫:Put()
- 為輸入的 Key 做 Hash 運算,得到 hash 值
- 通過 hash 值,定位到對應的 Segment 對象
- 獲取可重入鎖
- 再次通過 hash 值,定位到 Segment 當中數組的具體位置
- 插入或覆蓋 HashEntry 對象
- 釋放鎖
總結:可以看出ConcurrentHashMap在讀寫的時候都需要兩次定位。首先是定位到 Segment ,然后再定位到 Segment 下的具體的數組下標
六、size() 怎么 解決一致性問題
size()目的是統計ConcurrentHashMap的總元素數量,自然需要把各個Segment 內部的元素都加起來。但是在統計數量的時候,有可能 已經統計過的Segment順佳插入了新的元素,這個時候應該怎么辦?下面我們來看看ConcurrentHashMap的size(),他是一個嵌套循環,大致邏輯如下:
總結:一開始對 Segment 不加鎖,而是直接嘗試將所有的 Segment 元素中的count相加,這樣執行兩次,然后將兩次的結果進行對比,如果兩次結果相同則直接返回;如果不相同,則將所有的Segment加鎖,然后再次執行統計得到對應的 size 值。
七、再看看1.8是怎么實現的
1.8 拋棄了分段鎖(Segment),而是采取的是 CAS + Synchronized 的方式
將1.7里面的 HashEntry 改為 Node ,但是作用是一樣的。
先看看一個概念:樂觀鎖和悲觀鎖
悲觀鎖:認為對于同一個數據的并發操作,一定是為發生修改的
樂觀鎖:對于同一個數據的并發操作是不會發生修改的,在更新的時候會采取嘗試更新不斷嘗試的方式更新數據
CAS(compare and swap,比較交換)原理:
CAS有三個操作數,內存值V、預期值A、要修改的值B,當且僅當A和V相等時才會將V修改為B,否則什么也不會做。
CAS的缺點:
存在 ABA 問題,解決辦法,添加版本號
循環時間長,開銷大
只能保證一個共享變量的原子操作
總結
以上是生活随笔為你收集整理的聊聊ConcurrentHashMap的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Qt css样式大全(整理版)
- 下一篇: 【Android -- 性能优化】启动速