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