日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

Java集合篇:HashMap 与 ConcurrentHashMap 原理总结

發(fā)布時(shí)間:2024/9/30 java 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java集合篇:HashMap 与 ConcurrentHashMap 原理总结 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

一、HashMap原理總結(jié):

1、什么是HashMap:

(1)HashMap 是基于 Map 接口的非同步實(shí)現(xiàn),線程不安全,是為了快速存取而設(shè)計(jì)的;它采用 key-value 鍵值對(duì)的形式存放元素(并封裝成 Node 對(duì)象),允許使用 null 鍵和 null 值,但只允許存在一個(gè)鍵為 null,并且存放在 Node[0] 的位置,不過允許存在多個(gè) value 為 null 的情況。

(2)在 JDK7 及之前的版本,HashMap 的數(shù)據(jù)結(jié)構(gòu)可以看成“數(shù)組+鏈表”,在 JDK8 及之后的版本,數(shù)據(jù)結(jié)構(gòu)可以看成"數(shù)組+鏈表+紅黑樹",也就是說 HashMap ?底層采用數(shù)組實(shí)現(xiàn),數(shù)組的每個(gè)位置都存儲(chǔ)一個(gè)單向鏈表,當(dāng)鏈表的長(zhǎng)度超過一定的閾值時(shí),就會(huì)轉(zhuǎn)換成紅黑樹。轉(zhuǎn)換的目的是當(dāng)鏈表中元素較多時(shí),也能保證HashMap的存取效率(備注:鏈表轉(zhuǎn)為紅黑樹只有在數(shù)組的長(zhǎng)度大于等于64才會(huì)觸發(fā))

(3)HashMap 有兩個(gè)影響性能的關(guān)鍵參數(shù):“初始容量”和“加載因子”:

  • 容量 capacity:就是哈希表中數(shù)組的數(shù)量,默認(rèn)初始容量是16,容量必須是2的N次冪,這是為了提高計(jì)算機(jī)的執(zhí)行效率。
  • 加載因子 loadfactor:在 HashMap 擴(kuò)容之前,容量可以達(dá)到多滿的程度,默認(rèn)值為 0.75
  • 擴(kuò)容閾值 threshold = capacity * loadfactor

(4)采用 Fail-Fast 機(jī)制,底層通過一個(gè) modCount 值記錄修改的次數(shù),對(duì) HashMap 的修改操作都會(huì)增加這個(gè)值。迭代器在初始過程中會(huì)將這個(gè)值賦給 exceptedModCount ,在迭代的過程中,如果發(fā)現(xiàn) modCount 和 exceptedModCount 的值不一致,代表有其他線程修改了Map,就立刻拋出異常。

2、HashMap 的 put() 方法添加元素的過程:

(1)重新計(jì)算 hash 值:

拿到 key 的 hashcode 值之后,調(diào)用 hash() 方法重新計(jì)算 hash 值,防止質(zhì)量低下的 hashCode() 函數(shù)出現(xiàn),從而使 hash 值的分布盡量均勻。

JDK8 及之后的版本,對(duì) hash() 方法進(jìn)行了優(yōu)化,重新計(jì)算 hash 值時(shí),讓 hashCode 的高16位參與異或運(yùn)算,目的是即使 table 數(shù)組的長(zhǎng)度較小,在計(jì)算元素存儲(chǔ)位置時(shí),也能讓高位也參與運(yùn)算。

(key == null)? 0 : ( h = key.hashcode()) ^ (h >>> 16)

(2)計(jì)算元素存放在數(shù)組中的哪個(gè)位置:

將重新計(jì)算出來的 hash 值與 (tablel.length-1) 進(jìn)行位與&運(yùn)算,得出元素應(yīng)該放入數(shù)組的哪個(gè)位置。

為什么 HashMap 的底層數(shù)組長(zhǎng)度總是2的n次方冪?因?yàn)楫?dāng) length 為2的n次方時(shí),h & (length - 1) 就相當(dāng)于對(duì) length 取模,而且速度比直接取模要快得多,二者等價(jià)不等效,這是HashMap在性能上的一個(gè)優(yōu)化

(3)將 key-value 添加到數(shù)組中:

① 如果計(jì)算出的數(shù)組位置上為空,那么直接將這個(gè)元素插入放到該位置中。

② 如果數(shù)組該位置上已經(jīng)存在鏈表,則使用 equals() 比較鏈表上是否存在 key 相同的節(jié)點(diǎn),如果為true,則替換原元素;如果不存在,則在鏈表的尾部插入新節(jié)點(diǎn)(Jdk1.7及以前的版本使用的頭插法)

③ 如果插入元素后,如果鏈表的節(jié)點(diǎn)數(shù)是否超過8個(gè),則調(diào)用 treeifyBin() 將鏈表節(jié)點(diǎn)轉(zhuǎn)為紅黑樹節(jié)點(diǎn)。

④ 最后判斷 HashMap 總?cè)萘渴欠癯^閾值 threshold,則調(diào)用 resize() 方法進(jìn)行擴(kuò)容,擴(kuò)容后數(shù)組的長(zhǎng)度變成原來的2倍。

?在 HashMap 中,當(dāng)發(fā)生hash沖突時(shí),解決方式是采用拉鏈法,也就是將所有哈希值相同的記錄都放在同一個(gè)鏈表中,除此之外,解決hash沖突的方式有:

  • 開放地址法(線性探測(cè)再散列、二次探測(cè)再散列、偽隨機(jī)探測(cè)再散列):當(dāng)沖突發(fā)生時(shí),在散列表中形成一個(gè)探測(cè)序列,沿此序列逐個(gè)單元地查找,直到找到給定的關(guān)鍵字,或者碰到一個(gè)開放的地址為止(即該地址單元為空)。如果是插入的情況,在探查到開放的地址,則可將待插入的新結(jié)點(diǎn)存入該地址單元,如果是查找的情況,探查到開放的地址則表明表中無待查的關(guān)鍵字,即查找失敗。
  • 再哈希法:產(chǎn)生沖突時(shí),使用另外的哈希函數(shù)計(jì)算出一個(gè)新的哈希地址、直到?jīng)_突不再發(fā)生
  • 建立一個(gè)公共溢出區(qū):把沖突的記錄都放在另一個(gè)存儲(chǔ)空間,不放在表里面。

3、HashMap擴(kuò)容的過程:

(1)重新建立一個(gè)新的數(shù)組,長(zhǎng)度為原數(shù)組的兩倍;

(2)遍歷舊數(shù)組的每個(gè)數(shù)據(jù),重新計(jì)算每個(gè)元素在新數(shù)組中的存儲(chǔ)位置。使用節(jié)點(diǎn)的hash值與舊數(shù)組長(zhǎng)度進(jìn)行位與運(yùn)算,如果運(yùn)算結(jié)果為0,表示元素在新數(shù)組中的位置不變;否則,則在新數(shù)組中的位置下標(biāo)=原位置+原數(shù)組長(zhǎng)度。

(3)將舊數(shù)組上的每個(gè)數(shù)據(jù)使用尾插法逐個(gè)轉(zhuǎn)移到新數(shù)組中,并重新設(shè)置擴(kuò)容閾值。

問題:為什么擴(kuò)容時(shí)節(jié)點(diǎn)重 hash 只可能分布在原索引位置或者 原索引長(zhǎng)度+oldCap 位置呢?換句話說,擴(kuò)容時(shí)使用節(jié)點(diǎn)的hash值跟oldCap進(jìn)行位與運(yùn)算,以此決定將節(jié)點(diǎn)分布到原索引位置或者原索引+oldCap位置上的原理是什么呢?

假設(shè)老表的容量為16,則新表容量為16*2=32,假設(shè)節(jié)點(diǎn)1的hash值為 0000 0000 0000 0000 0000 1111 0000 1010,節(jié)點(diǎn)2的hash值為 0000 0000 0000 0000 0000 1111 0001 1010。

那么節(jié)點(diǎn)1和節(jié)點(diǎn)2在老表的索引位置計(jì)算如下圖計(jì)算1,由于老表的長(zhǎng)度限制,節(jié)點(diǎn)1和節(jié)點(diǎn)2的索引位置只取決于節(jié)點(diǎn)hash值的最后4位。再看計(jì)算2,計(jì)算2為元素在新表中的索引計(jì)算,可以看出如果兩個(gè)節(jié)點(diǎn)在老表的索引位置相同,則新表的索引位置只取決于節(jié)點(diǎn)hash值倒數(shù)第5位的值,而此位置的值剛好為老表的容量值16,此時(shí)節(jié)點(diǎn)在新表的索引位置只有兩種情況:原索引位置和 原索引+oldCap位置(在此例中即為10和10+16=26)。由于結(jié)果只取決于節(jié)點(diǎn)hash值的倒數(shù)第5位,而此位置的值剛好為老表的容量值16,因此此時(shí)新表的索引位置的計(jì)算可以替換為計(jì)算3,直接使用節(jié)點(diǎn)的hash值與老表的容量16進(jìn)行位于運(yùn)算,如果結(jié)果為0則該節(jié)點(diǎn)在新表的索引位置為原索引位置,否則該節(jié)點(diǎn)在新表的索引位置為 原索引+ oldCap 位置。

4、HashMap 鏈表轉(zhuǎn)換成紅黑樹:

當(dāng)數(shù)組中某個(gè)位置的節(jié)點(diǎn)達(dá)到8個(gè)時(shí),會(huì)觸發(fā) treeifyBin() 方法將鏈表節(jié)點(diǎn)(Node)轉(zhuǎn)紅黑樹節(jié)點(diǎn)(TreeNode,間接繼承Node),轉(zhuǎn)成紅黑樹節(jié)點(diǎn)后,其實(shí)鏈表的結(jié)構(gòu)還存在,通過next屬性維持,紅黑樹節(jié)點(diǎn)在進(jìn)行操作時(shí)都會(huì)維護(hù)鏈表的結(jié)構(gòu),并不是轉(zhuǎn)為紅黑樹節(jié)點(diǎn)后,鏈表結(jié)構(gòu)就不存在了。當(dāng)數(shù)組中某個(gè)位置的節(jié)點(diǎn)在移除后達(dá)到6個(gè)時(shí),并且該索引位置的節(jié)點(diǎn)為紅黑樹節(jié)點(diǎn),會(huì)觸發(fā) untreeify() 將紅黑樹節(jié)點(diǎn)轉(zhuǎn)化成鏈表節(jié)點(diǎn)。

HashMap 在進(jìn)行插入和刪除時(shí)有可能會(huì)觸發(fā)紅黑樹的插入平衡調(diào)整(balanceInsertion方法)或刪除平衡調(diào)整(balanceDeletion )方法,調(diào)整的方式主要有以下手段:左旋轉(zhuǎn)(rotateLeft方法)、右旋轉(zhuǎn)(rotateRight方法)、改變節(jié)點(diǎn)顏色(x.red = false、x.red = true),進(jìn)行調(diào)整的原因是為了維持紅黑樹的數(shù)據(jù)結(jié)構(gòu)。

當(dāng)鏈表長(zhǎng)過長(zhǎng)時(shí)會(huì)轉(zhuǎn)換成紅黑樹,那能不能使用AVL樹替代呢?

AVL樹是完全平衡二叉樹,要求每個(gè)結(jié)點(diǎn)的左右子樹的高度之差的絕對(duì)值最多為1,而紅黑樹通過適當(dāng)?shù)姆诺驮摋l件(紅黑樹限制從根到葉子的最長(zhǎng)的可能路徑不多于最短的可能路徑的兩倍長(zhǎng),結(jié)果是這個(gè)樹大致上是平衡的),以此來減少插入/刪除時(shí)的平衡調(diào)整耗時(shí),從而獲取更好的性能,雖然會(huì)導(dǎo)致紅黑樹的查詢會(huì)比AVL稍慢,但相比插入/刪除時(shí)獲取的時(shí)間,這個(gè)付出在大多數(shù)情況下顯然是值得的。

5、HashMap 在 JDK7 和 JDK8 有哪些區(qū)別?

① 數(shù)據(jù)結(jié)構(gòu):在 JDK7 及之前的版本,HashMap 的數(shù)據(jù)結(jié)構(gòu)可以看成“數(shù)組+鏈表”,在 JDK8 及之后的版本,數(shù)據(jù)結(jié)構(gòu)可以看成"數(shù)組+鏈表+紅黑樹",當(dāng)鏈表的長(zhǎng)度超過8時(shí),鏈表就會(huì)轉(zhuǎn)換成紅黑樹

② 對(duì)數(shù)據(jù)重哈希:JDK8 及之后的版本,對(duì) hash() 方法進(jìn)行了優(yōu)化,重新計(jì)算 hash 值時(shí),讓 hashCode 的高16位參與異或運(yùn)算,目的是在 table 的 length較小的時(shí)候,在進(jìn)行計(jì)算元素存儲(chǔ)位置時(shí),也讓高位也參與運(yùn)算。

③ 在 JDK7 及之前的版本,在添加元素的時(shí)候,采用頭插法,所以在擴(kuò)容的時(shí)候,會(huì)導(dǎo)致之前元素相對(duì)位置倒置了,在多線程環(huán)境下擴(kuò)容可能造成環(huán)形鏈表而導(dǎo)致死循環(huán)的問題。DK1.8之后使用的是尾插法,擴(kuò)容是不會(huì)改變?cè)氐南鄬?duì)位置

④ 擴(kuò)容時(shí)重新計(jì)算元素的存儲(chǔ)位置的方式:JDK7 及之前的版本重新計(jì)算存儲(chǔ)位置是直接使用 hash & (table.length-1);JDK8 使用節(jié)點(diǎn)的hash值與舊數(shù)組長(zhǎng)度進(jìn)行位與運(yùn)算,如果運(yùn)算結(jié)果為0,表示元素在新數(shù)組中的位置不變;否則,則在新數(shù)組中的位置下標(biāo)=原位置+原數(shù)組長(zhǎng)度。

6、線程不安全的體現(xiàn)?如何變成線程安全:

無論在 JDK7 還是 JDK8 的版本中,HashMap 都是線程不安全的,HashMap 的線程不安全主要體現(xiàn)在以下兩個(gè)方面:

  • 在JDK7及以前的版本,表現(xiàn)為在多線程環(huán)境下進(jìn)行擴(kuò)容,由于采用頭插法,位于同一索引位置的節(jié)點(diǎn)順序會(huì)反掉,導(dǎo)致可能出現(xiàn)死循環(huán)的情況
  • 在JDK8及以后的版本,表現(xiàn)為在多線程環(huán)境下添加元素,可能會(huì)出現(xiàn)數(shù)據(jù)丟失的情況

如果想使用線程安全的 Map 容器,可以使用以下幾種方式:

(1)使用線程安全的 Hashtable,它底層的每個(gè)方法都使用了 synchronized 保證線程同步,所以每次都鎖住整張表,在性能方面會(huì)相對(duì)比較低。

除了線程安全性方面,Hashtable 和 HashMap 的不同之處還有:

  • 繼承的父類:兩者都實(shí)現(xiàn)了 Map 接口,但 HashMap 繼承自 AbstractMap 類,而 Hashtable 繼承自 Dictionary 類
  • 遍歷方式:HashMap 僅支持 Iterator 的遍歷方式,但 Hashtable 實(shí)現(xiàn)了 Enumeration 接口,所以支持Iterator和Enumeration兩種遍歷方式
  • 使用方式:HashMap 允許 null 鍵和 null 值,Hashtable 不允許 null? 鍵和 null?值
  • 數(shù)據(jù)結(jié)構(gòu):HashMap 底層使用“數(shù)組+鏈表+紅黑樹”,Hashtable 底層使用“數(shù)組+鏈表”
  • 初始容量及擴(kuò)容方式:HashMap 的默認(rèn)初始容量為16,每次擴(kuò)容為原來的2倍;Hashtable 默認(rèn)初始容量為11,每次擴(kuò)容為原來的2倍+1。
  • 元素的hash值:HashMap的hash值是重新計(jì)算過的,Hashtable直接使用Object的hashCode;

之所以會(huì)出現(xiàn)初始容量以及元素hash值計(jì)算方式的不同,是因?yàn)?HashMap 和 Hashtable 設(shè)計(jì)時(shí)的側(cè)重點(diǎn)不同。Hashtable 的側(cè)重點(diǎn)是哈希結(jié)果更加均勻,使得哈希沖突減少,當(dāng)哈希表的大小為素?cái)?shù)時(shí),簡(jiǎn)單的取模哈希的結(jié)果會(huì)更加均勻。而 HashMap 則更加關(guān)注哈希的計(jì)算效率問題,在取模計(jì)算時(shí),如果模數(shù)是2的冪,那么我們可以直接使用位運(yùn)算來得到結(jié)果,效率要大大高于做除法。

有關(guān) Hashtable 的內(nèi)容,可以詳細(xì)閱讀:Hashtable原理詳解(JDK1.8)

(2)使用Collections.synchronizedMap()方法來獲取一個(gè)線程安全的集合,底層原理是使用synchronized來保證線程同步。

(3)使用 ConcurrentHashMap 集合。

JDK7 版本的 HahsMap 源碼解析:https://blog.csdn.net/a745233700/article/details/83108880

JDK8 版本的 HahsMap 源碼解析:https://blog.csdn.net/a745233700/article/details/85126716

二、ConcurrentHashMap 原理(JDK8):

1、ConcurrentHashMap 的實(shí)現(xiàn)原理:

在 JDK8 及以上的版本中,ConcurrentHashMap 的底層數(shù)據(jù)結(jié)構(gòu)依然采用“數(shù)組+鏈表+紅黑樹”,但是在實(shí)現(xiàn)線程安全性方面,拋棄了 JDK7 版本的 Segment分段鎖的概念,而是采用了 synchronized + CAS 算法來保證線程安全。在ConcurrentHashMap中,大量使用 Unsafe.compareAndSwapXXX 的方法,這類方法是利用一個(gè)CAS算法實(shí)現(xiàn)無鎖化的修改值操作,可以大大減少使用加鎖造成的性能消耗。這個(gè)算法的基本思想就是不斷比較當(dāng)前內(nèi)存中的變量值和你預(yù)期變量值是否相等,如果相等,則接受修改的值,否則拒絕你的而操作。因?yàn)楫?dāng)前線程中的值已經(jīng)不是最新的值,你的修改很可能會(huì)覆蓋掉其他線程修改的結(jié)果。

2、擴(kuò)容方法 transfer():

ConcurrentHashMap 為了減少擴(kuò)容帶來的時(shí)間影響,在擴(kuò)容過程中沒有進(jìn)行加鎖,并且支持多線程進(jìn)行擴(kuò)容操作。在擴(kuò)容過程中主要使用 sizeCtl 和 transferIndex 這兩個(gè)屬性來協(xié)調(diào)多線程之間的并發(fā)操作,并且在擴(kuò)容過程中大部分?jǐn)?shù)據(jù)可以做到訪問不阻塞,整個(gè)擴(kuò)容操作分為以下幾個(gè)步驟:

2.1、根據(jù) CPU 核數(shù)和數(shù)組長(zhǎng)度,計(jì)算每個(gè)線程應(yīng)該處理的桶數(shù)量,如果CPU為單核,則使用一個(gè)線程處理所有桶

2.2、根據(jù)當(dāng)前數(shù)組長(zhǎng)度n,新建一個(gè)兩倍長(zhǎng)度的數(shù)組 nextTable(該這個(gè)步驟是單線程執(zhí)行的)

2.3、將原來 table 中的元素復(fù)制到 nextTable 中,這里允許多線程進(jìn)行操作,具體操作步驟如下:

(1)初始化 ForwardingNode 對(duì)象,充當(dāng)占位節(jié)點(diǎn),hash 值為 -1,該占位對(duì)象存在時(shí)表示集合正在擴(kuò)容狀態(tài)。

ForwardingNode 的 key、value、next 屬性均為 null ,nextTable 屬性指向擴(kuò)容后的數(shù)組,它的作用主要有以下兩個(gè):

  • 占位作用,用于標(biāo)識(shí)數(shù)組該位置的桶已經(jīng)遷移完畢
  • 作為一個(gè)轉(zhuǎn)發(fā)的作用,擴(kuò)容期間如果遇到查詢操作,遇到轉(zhuǎn)發(fā)節(jié)點(diǎn),會(huì)把該查詢操作轉(zhuǎn)發(fā)到新的數(shù)組上去,不會(huì)阻塞查詢操作。

(2)通過 for 循環(huán)從右往左依次遷移當(dāng)前線程所負(fù)責(zé)數(shù)組:

① 如果當(dāng)前桶沒有元素,則直接通過 CAS 放置一個(gè) ForwardingNode 占位對(duì)象,以便查詢操作的轉(zhuǎn)發(fā)和標(biāo)識(shí)當(dāng)前位置已經(jīng)被處理過。

② 如果線程遍歷到節(jié)點(diǎn)的 hash 值為 MOVE,也就是 -1(即 ForwardingNode 節(jié)點(diǎn)),則直接跳過,繼續(xù)處理下一個(gè)桶中的節(jié)點(diǎn)

③ 如果不滿足上面兩種情況,則直接給當(dāng)前桶節(jié)點(diǎn)加上 synchronized 鎖,然后重新計(jì)算該桶的元素在新數(shù)組中的應(yīng)該存放的位置,并進(jìn)行數(shù)據(jù)遷移。重計(jì)算節(jié)點(diǎn)的存放位置時(shí),通過 CAS 把低位節(jié)點(diǎn) lowNode 設(shè)置到新數(shù)組的 i 位置,高位節(jié)點(diǎn) highNode 設(shè)置到 i+n 的位置(i 表示在原數(shù)組的位置,n表示原數(shù)組的長(zhǎng)度)

  • 如果數(shù)組中的節(jié)點(diǎn)是鏈表結(jié)構(gòu),則順序遍歷鏈表并使用頭插法進(jìn)行構(gòu)造新鏈表
  • 如果數(shù)組中的節(jié)點(diǎn)是紅黑樹結(jié)構(gòu),則for循環(huán)以鏈表方式遍歷整棵紅黑樹,使用尾插法拼接

④ 當(dāng)前桶位置的數(shù)據(jù)遷移完成后,將 ForwardingNode 占位符對(duì)象設(shè)置到當(dāng)前桶位置上,表示該位置已經(jīng)被處理了

2.4、每當(dāng)一條線程擴(kuò)容結(jié)束就會(huì)更新一次 sizeCtl 的值,進(jìn)行減 1 操作,當(dāng)最后一條線程擴(kuò)容結(jié)束后,需要重新檢查一遍數(shù)組,防止有遺漏未成功遷移的桶。擴(kuò)容結(jié)束后,將 nextTable 設(shè)置為 null,表示擴(kuò)容已結(jié)束,將 table 指向新數(shù)組,sizeCtl 設(shè)置為擴(kuò)容閾值。

sizeCtl:是一個(gè)控制標(biāo)識(shí)符,在不同的地方有不同用途,它不同的取值不同也代表不同的含義。在擴(kuò)容時(shí),它代表的是當(dāng)前并發(fā)擴(kuò)容的線程數(shù)量

  • 負(fù)數(shù)代表正在進(jìn)行初始化或擴(kuò)容操作:-1代表正在初始化,-N 表示有N-1個(gè)線程正在進(jìn)行擴(kuò)容操作
  • 正數(shù)或0代表hash表還沒有被初始化或下一次進(jìn)行擴(kuò)容的大小,這一點(diǎn)類似于擴(kuò)容閾值的概念。

3、put() 方法的 helpTransfer() 協(xié)助擴(kuò)容:

put() 在多線程情況下可能有以下兩種情況:

  • 如果檢測(cè)到 ConcurrentHashMap 正在進(jìn)行擴(kuò)容操作,也就是當(dāng)前桶位置上被插入了 ForwardingNode 節(jié)點(diǎn),那么當(dāng)前線程也要協(xié)助進(jìn)行擴(kuò)容,協(xié)助擴(kuò)容時(shí)會(huì)調(diào)用 helpTransfer() 方法,當(dāng)方法被調(diào)用的時(shí)候,當(dāng)前 ConcurrentHashMap 一定已經(jīng)有了 nextTable 對(duì)象,首先拿到這個(gè) nextTable 對(duì)象,調(diào)用transfer方法。
  • 如果檢測(cè)到要插入的節(jié)點(diǎn)是非空且不是 ForwardingNode ?節(jié)點(diǎn),就對(duì)這個(gè)節(jié)點(diǎn)加鎖,這樣就保證了線程安全。盡管這個(gè)有一些影響效率,但是還是會(huì)比hashTable 的 synchronized 要好得多。

有關(guān)擴(kuò)容的源碼解析可以閱讀該文章:https://blog.csdn.net/ZOKEKAI/article/details/90051567

三、ConcurrentHashMap 原理(JDK7):

在第二部分我們提到 JDK8 和 JDK7 中 ConcurrentHashMap 的設(shè)計(jì)是不一樣的,那么下面我們就簡(jiǎn)單介紹一下 JDK7 中 ConcurrentHashMap 的實(shí)現(xiàn):

1、ConcurrentHashMap實(shí)現(xiàn)的原理:

在 JDK7 中,ConcurrentHashMap 使用“分段鎖”機(jī)制實(shí)現(xiàn)線程安全,數(shù)據(jù)結(jié)構(gòu)可以看成是"Segment數(shù)組+HashEntry數(shù)組+鏈表",一個(gè) ConcurrentHashMap 實(shí)例中包含若干個(gè) Segment 實(shí)例組成的數(shù)組,每個(gè) Segment 實(shí)例又包含由若干個(gè)桶,每個(gè)桶中都是由若干個(gè) HashEntry 對(duì)象鏈接起來的鏈表。

因?yàn)镾egment 類繼承 ReentrantLock 類,所以能充當(dāng)鎖的角色,通過 segment 段將 ConcurrentHashMap 劃分為不同的部分,就可以使用不同的鎖來控制對(duì)哈希表不同部分的修改,從而允許多個(gè)寫操作并發(fā)進(jìn)行,默認(rèn)支持 16 個(gè)線程執(zhí)行并發(fā)寫操作,及任意數(shù)量線程的讀操作。

?2、ConcurrentHashMap 的 put() 方法添加元素的過程:

(1)對(duì) key 值進(jìn)行重哈希,并使用重哈希的結(jié)果與 segmentFor() 方法, 計(jì)算出元素具體分到哪個(gè) segment 中。插入元素前,先使用 lock() 對(duì)該 segment 加鎖,之后再使用頭插法插入元素。如果其他線程經(jīng)過計(jì)算也是放在這個(gè) segment 下,則需要先獲取鎖,如果計(jì)算得出放在其他的 segment,則正常執(zhí)行,不會(huì)影響效率,以此實(shí)現(xiàn)線程安全,這樣就能夠保證只要多個(gè)修改操作不發(fā)生在同一個(gè) segment ?時(shí),它們就可以并發(fā)進(jìn)行。

(2)在將元素插入到 segment 前,會(huì)檢查本次插入會(huì)不會(huì)導(dǎo)致 segment 中元素的數(shù)量超過閾值,如果會(huì),那么就先對(duì) segment 進(jìn)行擴(kuò)容和重哈希操作,然后再進(jìn)行插入。而重哈希操作,實(shí)際上是對(duì) ConcurrentHashMap 的某個(gè) segment 的重哈希,因此 ConcurrentHashMap 的每個(gè) segment 段所包含的桶位也就不盡相同。

如果元素的 hash 值與原數(shù)組長(zhǎng)度進(jìn)行位與運(yùn)算,得到的結(jié)果為0,那么元素在新桶的序號(hào)就是和原桶的序號(hào)是相等的;否則元素在新桶的序號(hào)就是原桶的序號(hào)加上原數(shù)組的長(zhǎng)度

3、ConcurrentHashMap 讀操作為什么不需要加鎖?

(1)在 HashEntry 類中,key,hash 和 next 域都被聲明為 final 的,value 域被 volatile 所修飾,因此 HashEntry 對(duì)象幾乎是不可變的,通過 HashEntry 對(duì)象的不變性來降低讀操作對(duì)加鎖的需求

next 域被聲明為 final,意味著不能從hash鏈的中間或尾部添加或刪除節(jié)點(diǎn),因?yàn)檫@需要修改 next 引用值,因此所有的節(jié)點(diǎn)的修改只能從頭部開始。但是對(duì)于 remove 操作,需要將要?jiǎng)h除節(jié)點(diǎn)的前面所有節(jié)點(diǎn)整個(gè)復(fù)制一遍,最后一個(gè)節(jié)點(diǎn)指向要?jiǎng)h除結(jié)點(diǎn)的下一個(gè)結(jié)點(diǎn)。

(2)用 volatile 變量協(xié)調(diào)讀寫線程間的內(nèi)存可見性;

(3)若讀時(shí)發(fā)生指令重排序現(xiàn)象(也就是讀到 value 域的值為 null 的時(shí)候),則加鎖重讀;

4、ConcurrentHashMap 的跨端操作:

ConcurrentHashMap 的跨段操作:比如 size() 計(jì)算集合中元素的總個(gè)數(shù)。首先為了并發(fā)性的考慮,ConcurrentHashMap 并沒有使用全局計(jì)數(shù)器,而是分別在每個(gè) segment 中使用一個(gè) volatile 修飾的計(jì)數(shù)器count,這樣當(dāng)需要更新計(jì)數(shù)器時(shí),不用鎖定整個(gè) ConcurrentHashMap。而 size() 在統(tǒng)計(jì)時(shí),是先嘗試 RETRIES_BEFORE_LOCK 次(默認(rèn)是兩次)通過不鎖住 Segment 的方式來統(tǒng)計(jì)各個(gè) Segment 大小,如果統(tǒng)計(jì)的過程中,容器的count發(fā)生了變化,則再采用對(duì)所有 segment 段加鎖的方式來統(tǒng)計(jì)所有Segment的大小。

JDK7 版本的 ConcurrentHashMap 源碼解析:https://blog.csdn.net/a745233700/article/details/83120464

JDK8 版本的 ConcurrentHashMap 源碼解析:https://blog.csdn.net/a745233700/article/details/83123359

總結(jié)

以上是生活随笔為你收集整理的Java集合篇:HashMap 与 ConcurrentHashMap 原理总结的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。