高并發(fā)神器!ConcurrentHashMap為何如此高效?
引言
大家好,我是小米!今天我們來(lái)聊聊Java中一個(gè)超級(jí)實(shí)用的線程安全集合類(lèi)——ConcurrentHashMap。對(duì)于多線程環(huán)境中需要頻繁讀寫(xiě)數(shù)據(jù)的場(chǎng)景來(lái)說(shuō),ConcurrentHashMap無(wú)疑是個(gè)好幫手。那么,為什么ConcurrentHashMap效率高?底層實(shí)現(xiàn)的奧秘又是什么?接下來(lái),讓我們一探究竟。
圖片
ConcurrentHashMap與Hashtable的對(duì)比
在多線程環(huán)境中,我們常常需要保證數(shù)據(jù)的線程安全性。說(shuō)到實(shí)現(xiàn)線程安全,ConcurrentHashMap和Hashtable都是不錯(cuò)的選擇,但二者的性能表現(xiàn)卻有很大差異。
Hashtable:同步鎖的性能瓶頸
Hashtable作為Java早期的線程安全類(lèi),主要通過(guò)Synchronized關(guān)鍵字進(jìn)行方法級(jí)別的同步來(lái)保證線程安全。比如,在執(zhí)行put或get操作時(shí),Hashtable會(huì)鎖住整個(gè)對(duì)象,導(dǎo)致同一時(shí)間只能有一個(gè)線程訪問(wèn)或修改數(shù)據(jù)。這樣雖然保證了安全性,但性能相對(duì)低下。
ConcurrentHashMap:分段鎖的高效設(shè)計(jì)
ConcurrentHashMap的核心思想是分段鎖,這使得它在性能上要遠(yuǎn)優(yōu)于Hashtable。簡(jiǎn)單來(lái)說(shuō),ConcurrentHashMap將數(shù)據(jù)劃分成多個(gè)段(Segment),每個(gè)Segment對(duì)應(yīng)一個(gè)鎖。不同線程訪問(wèn)不同Segment的數(shù)據(jù)時(shí),可以同時(shí)進(jìn)行而不互相阻塞,從而提高了并發(fā)性能。
Java的兩個(gè)主要版本(1.7和1.8)對(duì)ConcurrentHashMap的底層結(jié)構(gòu)有很大的差別,我們一起來(lái)看看它們的演變過(guò)程。
JDK 1.7:Segment分段鎖
在JDK 1.7中,ConcurrentHashMap使用了分段鎖(Segment)的設(shè)計(jì)。通過(guò)這一設(shè)計(jì),ConcurrentHashMap達(dá)到了提高并發(fā)訪問(wèn)率的效果。
底層結(jié)構(gòu):Segment數(shù)組 + HashEntry鏈表
ConcurrentHashMap在底層將數(shù)據(jù)分為多個(gè)Segment,每個(gè)Segment內(nèi)部由鏈表存儲(chǔ)數(shù)據(jù)。這樣一來(lái),ConcurrentHashMap將整個(gè)Map分成了若干個(gè)小的子Map,每個(gè)Segment相當(dāng)于一個(gè)小的Hashtable,持有一個(gè)獨(dú)立的鎖。因此,多個(gè)線程訪問(wèn)不同Segment的元素時(shí)不會(huì)相互影響,從而提高了并發(fā)性能。
如何實(shí)現(xiàn)分段鎖?
ConcurrentHashMap中會(huì)對(duì)每一個(gè)鍵值對(duì)進(jìn)行哈希計(jì)算,以確定它屬于哪個(gè)Segment。每個(gè)Segment鎖住一個(gè)區(qū)域的數(shù)據(jù),這樣每次只鎖定一個(gè)Segment,即使一個(gè)Segment被鎖定,其他Segment也可以同時(shí)被訪問(wèn),這就避免了整個(gè)Map鎖住的低效情況。
優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):提高了并發(fā)性能,多個(gè)線程可以同時(shí)操作不同的Segment。
- 缺點(diǎn):Segment數(shù)量(默認(rèn)16個(gè))固定后,無(wú)法動(dòng)態(tài)擴(kuò)容。即使并發(fā)再高,也無(wú)法突破這個(gè)限制。
JDK 1.8:無(wú)Segment,鏈表+紅黑樹(shù)+CAS
JDK 1.8中,ConcurrentHashMap的底層結(jié)構(gòu)和實(shí)現(xiàn)方式發(fā)生了重大變化,Segment不再存在,取而代之的是更為精簡(jiǎn)的實(shí)現(xiàn)方式。JDK 1.8摒棄了Segment鎖機(jī)制,而是采用了數(shù)組+鏈表+紅黑樹(shù)的組合數(shù)據(jù)結(jié)構(gòu)。
數(shù)據(jù)結(jié)構(gòu):Node數(shù)組 + 鏈表/紅黑樹(shù)
JDK 1.8的ConcurrentHashMap與1.8版本的HashMap非常相似,底層通過(guò)一個(gè)Node數(shù)組來(lái)存儲(chǔ)數(shù)據(jù)。如果某個(gè)桶中有大量hash沖突的數(shù)據(jù),會(huì)先形成鏈表;當(dāng)鏈表長(zhǎng)度超過(guò)一定閾值(8)后,會(huì)轉(zhuǎn)化成紅黑樹(shù)結(jié)構(gòu),從而提高查詢效率。
并發(fā)控制:CAS + synchronized
ConcurrentHashMap 1.8 的線程安全主要通過(guò)CAS(Compare And Swap)和synchronized關(guān)鍵字來(lái)實(shí)現(xiàn),而不是之前的鎖住整個(gè)Segment。這樣在進(jìn)行增刪改查時(shí),只需要鎖住當(dāng)前操作的鏈表頭部節(jié)點(diǎn)即可,大大降低了鎖的粒度,進(jìn)一步提升了并發(fā)效率。
- CAS機(jī)制:CAS在檢測(cè)到變量未被其他線程修改時(shí),直接更新變量的值。相比傳統(tǒng)的鎖機(jī)制,CAS可以在無(wú)鎖的情況下完成并發(fā)更新,大大提高了效率。
- synchronized:當(dāng)CAS無(wú)法保證安全性時(shí),才會(huì)退而采用synchronized進(jìn)行保護(hù)。JDK 1.8通過(guò)這種靈活的設(shè)計(jì),進(jìn)一步提升了并發(fā)性能。
優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):性能較JDK 1.7更優(yōu),不再依賴(lài)Segment;鎖的粒度進(jìn)一步縮小。
- 缺點(diǎn):實(shí)現(xiàn)較復(fù)雜,對(duì)內(nèi)存占用和系統(tǒng)資源提出了更高的要求。
ConcurrentHashMap的核心機(jī)制剖析
1. get操作
get操作在ConcurrentHashMap中是無(wú)鎖的,主要通過(guò)定位到具體的Node節(jié)點(diǎn)來(lái)直接獲取數(shù)據(jù)。
流程:
- 首先通過(guò)hash值確定數(shù)據(jù)的位置。
- 若找到的桶是鏈表,則遍歷鏈表尋找對(duì)應(yīng)的節(jié)點(diǎn)。
- 若桶內(nèi)為紅黑樹(shù),則使用樹(shù)的查找邏輯獲取目標(biāo)節(jié)點(diǎn)。
2. put操作
在執(zhí)行put時(shí),ConcurrentHashMap會(huì)嘗試使用CAS來(lái)添加元素。如果當(dāng)前節(jié)點(diǎn)位置為空,CAS更新會(huì)成功;否則,系統(tǒng)會(huì)退而使用synchronized鎖住節(jié)點(diǎn)進(jìn)行更新操作。
流程:
- 計(jì)算key的hash值,定位到具體的桶。
- 若該位置為空,則使用CAS將新值插入。
- 若該位置已存在數(shù)據(jù):
若為鏈表,遍歷鏈表并添加至末尾;鏈表長(zhǎng)度超過(guò)8則轉(zhuǎn)化為紅黑樹(shù)。
若為紅黑樹(shù),則按照紅黑樹(shù)的插入規(guī)則進(jìn)行更新。
- 如果容量超過(guò)閾值,則觸發(fā)擴(kuò)容。
3. 擴(kuò)容機(jī)制
與HashMap類(lèi)似,ConcurrentHashMap在容量不足時(shí)會(huì)進(jìn)行擴(kuò)容。不同的是,ConcurrentHashMap的擴(kuò)容操作是分段進(jìn)行的。
- 分段擴(kuò)容:在擴(kuò)容過(guò)程中,多個(gè)線程可以協(xié)作進(jìn)行桶數(shù)據(jù)遷移,而不是一個(gè)線程獨(dú)自完成擴(kuò)容,從而減少了線程阻塞。
ConcurrentHashMap的優(yōu)勢(shì)總結(jié)
- 高并發(fā)性能:JDK 1.8后的ConcurrentHashMap通過(guò)CAS操作和synchronized,避免了全面鎖的低效問(wèn)題,鎖的粒度更小,提高了整體并發(fā)性。
- 高效數(shù)據(jù)結(jié)構(gòu):引入紅黑樹(shù),提升了查詢效率,使得沖突嚴(yán)重的情況下,依然能保持較高的訪問(wèn)效率。
- 分段擴(kuò)容:擴(kuò)容過(guò)程可由多個(gè)線程協(xié)作進(jìn)行,進(jìn)一步提升了多線程環(huán)境下的性能表現(xiàn)。
END
ConcurrentHashMap作為Java中一個(gè)重要的并發(fā)集合類(lèi),憑借其分段鎖和CAS機(jī)制,在保證線程安全的同時(shí),大大提升了性能。JDK 1.7中通過(guò)Segment的分段鎖來(lái)降低鎖競(jìng)爭(zhēng),而JDK 1.8中則進(jìn)一步改進(jìn)為無(wú)鎖化操作和紅黑樹(shù)的結(jié)構(gòu),大幅度提升了性能和并發(fā)性。
在實(shí)際開(kāi)發(fā)中,如果你需要一個(gè)線程安全、高并發(fā)的Map集合,ConcurrentHashMap絕對(duì)是一個(gè)值得信賴(lài)的選擇!希望今天的分享能夠幫助大家更好地理解ConcurrentHashMap的底層設(shè)計(jì)及其優(yōu)點(diǎn),咱們下次再一起探討更多Java黑科技!