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