HashMap與ConcurrentHashMap要點

HashMap
HashMap底層數據結構
JDK=中的HashMap爲什麼要使⽤紅⿊樹?
JDK=中的HashMap什麼時候將鏈表轉化爲紅⿊樹?
JDK=中HashMap的put⽅法的實現過程?
JDK=中HashMap的get⽅法的實現過程
JDK<與JDK=中HashMap的不同點
ConcurrentHashMap
JDK<中的ConcurrentHashMap是怎麼保證併發安全的?
JDK<中的ConcurrentHashMap的底層原理
JDK=中的ConcurrentHashMap是怎麼保證併發安全的?
JDK<和JDK=中的ConcurrentHashMap的不同點
HashMap
HashMap底層數據結構
JDK7:數組+鏈表
JDK8: 數組+鏈表+紅⿊樹(看過源碼的同學應該知道JDK8中即使⽤了單向鏈表,也使⽤了雙向鏈表,雙
向鏈表主要是爲了鏈表操作⽅便,應該在插⼊,擴容,鏈表轉紅⿊樹,紅⿊樹轉鏈表的過程中都要操作鏈
表)
JDK8中的HashMap爲什麼要使⽤紅⿊樹?
當元素個數⼩於⼀個閾值時,鏈表整體的插⼊查詢效率要⾼於紅⿊樹,當元素個數⼤於此閾值時,鏈表整
體的插⼊查詢效率要低於紅⿊樹。此閾值在HashMap中爲8
JDK8中的HashMap什麼時候將鏈表轉化爲紅⿊樹?
這個題很容易答錯,⼤部分答案就是:當鏈表中的元素個數⼤於8時就會把鏈表轉化爲紅⿊樹。但是其實還
有另外⼀個限制:當發現鏈表中的元素個數⼤於8之後,還會判斷⼀下當前數組的⻓度,如果數組⻓度⼩於
164時,此時並不會轉化爲紅⿊樹,⽽是進⾏擴容。只有當鏈表中的元素個數⼤於8,並且數組的⻓度⼤於
等於64時纔會將鏈表轉爲紅⿊樹。
上⾯擴容的原因是,如果數組⻓度還⽐較⼩,就先利⽤擴容來縮⼩鏈表的⻓度。
JDK8中HashMap的put⽅法的實現過程?
1. 根據key⽣成hashcode
2. 判斷當前HashMap對象中的數組是否爲空,如果爲空則初始化該數組
3. 根據邏輯與運算,算出hashcode基於當前數組對應的數組下標i
4. 判斷數組的第i個位置的元素(tab[i])是否爲空
a. 如果爲空,則將key,value封裝爲Node對象賦值給tab[i]
b. 如果不爲空:
i. 如果put⽅法傳⼊進來的key等於tab[i].key,那麼證明存在相同的key
ii. 如果不等於tab[i].key,則:
1. 如果tab[i]的類型是TreeNode,則表示數組的第i位置上是⼀顆紅⿊樹,那麼將key和
value插⼊到紅⿊樹中,並且在插⼊之前會判斷在紅⿊樹中是否存在相同的key
2. 如果tab[i]的類型不是TreeNode,則表示數組的第i位置上是⼀個鏈表,那麼遍歷鏈表尋
找是否存在相同的key,並且在遍歷的過程中會對鏈表中的結點數進⾏計數,當遍歷到最
後⼀個結點時,會將key,value封裝爲Node插⼊到鏈表的尾部,同時判斷在插⼊新結點之
前的鏈表結點個數是不是⼤於等於8,如果是,則將鏈表改爲紅⿊樹。
iii. 如果上述步驟中發現存在相同的key,則根據onlyIfAbsent標記來判斷是否需要更新value值,
然後返回oldValue
5. modCount++
6. HashMap的元素個數size加1
7. 如果size⼤於擴容的閾值,則進⾏擴容
JDK8中HashMap的get⽅法的實現過程
1. 根據key⽣成hashcode
2. 如果數組爲空,則直接返回空
3. 如果數組不爲空,則利⽤hashcode和數組⻓度通過邏輯與操作算出key所對應的數組下標i
4. 如果數組的第i個位置上沒有元素,則直接返回空
5. 如果數組的第1個位上的元素的key等於get⽅法所傳進來的key,則返回該元素,並獲取該元素的value
6. 如果不等於則判斷該元素還有沒有下⼀個元素,如果沒有,返回空
7. 如果有則判斷該元素的類型是鏈表結點還是紅⿊樹結點
a. 如果是鏈表則遍歷鏈表
b. 如果是紅⿊樹則遍歷紅⿊樹
8. 找到即返回元素,沒找到的則返回空
2JDK7與JDK8中HashMap的不同點
1. JDK8中使⽤了紅⿊樹
2. JDK7中鏈表的插⼊使⽤的頭插法(擴容轉移元素的時候也是使⽤的頭插法,頭插法速度更快,⽆需遍
歷鏈表,但是在多線程擴容的情況下使⽤頭插法會出現循環鏈表的問題,導致CPU飆升),JDK8中鏈
表使⽤的尾插法(JDK8中反正要去計算鏈表當前結點的個數,反正要遍歷的鏈表的,所以直接使⽤尾
插法)
3. JDK7的Hash算法⽐JDK8中的更復雜,Hash算法越複雜,⽣成的hashcode則更散列,那麼hashmap
中的元素則更散列,更散列則hashmap的查詢性能更好,JDK7中沒有紅⿊樹,所以只能優化Hash算
法使得元素更散列,⽽JDK8中增加了紅⿊樹,查詢性能得到了保障,所以可以簡化⼀下Hash算法,畢
竟Hash算法越複雜就越消耗CPU
4. 擴容的過程中JDK7中有可能會重新對key進⾏哈希(重新Hash跟哈希種⼦有關係),⽽JDK8中沒有這
部分邏輯
5. JDK8中擴容的條件和JDK7中不⼀樣,除開判斷size是否⼤於閾值之外,JDK7中還判斷了tab[i]是否爲
空,不爲空的時候纔會進⾏擴容,⽽JDK8中則沒有該條件了
6. JDK8中還多了⼀個API:putIfAbsent(key,value)
7. JDK7和JDK8擴容過程中轉移元素的邏輯不⼀樣,JDK7是每次轉移⼀個元素,JDK8是先算出來當前位
置上哪些元素在新數組的低位上,哪些在新數組的⾼位上,然後在⼀次性轉移
ConcurrentHashMap
JDK7中的ConcurrentHashMap是怎麼保證併發安全的?
主要利⽤Unsafe操作+ReentrantLock+分段思想
主要使⽤了Unsafe操作中的:
1. compareAndSwapObject:通過cas的⽅式修改對象的屬性
2. putOrderedObject:併發安全的給數組的某個位置賦值
3. getObjectVolatile:併發安全的獲取數組某個位置的元素
分段思想是爲了提⾼ConcurrentHashMap的併發量,分段數越⾼則⽀持的最⼤併發量越⾼,程序員可以
通過concurrencyLevel參數來指定併發量。ConcurrentHashMap的內部類Segment就是⽤來表示某⼀個
段的。
每個Segment就是⼀個⼩型的HashMap的,當調⽤ConcurrentHashMap的put⽅法是,最終會調⽤到
Segment的put⽅法,⽽Segment類繼承了ReentrantLock,所以Segment⾃帶可重⼊鎖,當調⽤到
Segment的put⽅法時,會先利⽤可重⼊鎖加鎖,加鎖成功後再將待插⼊的key,value插⼊到⼩型HashMap
中,插⼊完成後解鎖。
JDK7中的ConcurrentHashMap的底層原理
3ConcurrentHashMap底層是由兩層嵌套數組來實現的:
1. ConcurrentHashMap對象中有⼀個屬性segments,類型爲Segment[];
2. Segment對象中有⼀個屬性table,類型爲HashEntry[];
當調⽤ConcurrentHashMap的put⽅法時,先根據key計算出對應的Segment[]的數組下標j,確定好當前
key,value應該插⼊到哪個Segment對象中,如果segments[j]爲空,則利⽤⾃旋鎖的⽅式在j位置⽣成⼀個
Segment對象。
然後調⽤Segment對象的put⽅法。
Segment對象的put⽅法會先加鎖,然後也根據key計算出對應的HashEntry[]的數組下標i,然後將
key,value封裝爲HashEntry對象放⼊該位置,此過程和JDK7的HashMap的put⽅法⼀樣,然後解鎖。
在加鎖的過程中邏輯⽐較複雜,先通過⾃旋加鎖,如果超過⼀定次數就會直接阻塞等等加鎖。(具體流程
請求看vip視頻.)
JDK8中的ConcurrentHashMap是怎麼保證併發安全的?
主要利⽤Unsafe操作+synchronized關鍵字。
Unsafe操作的使⽤仍然和JDK7中的類似,主要負責併發安全的修改對象的屬性或數組某個位置的值。
synchronized主要負責在需要操作某個位置時進⾏加鎖(該位置不爲空),⽐如向某個位置的鏈表進⾏插
⼊結點,向某個位置的紅⿊樹插⼊結點。
JDK8中其實仍然有分段鎖的思想,只不過JDK7中段數是可以控制的,⽽JDK8中是數組的每⼀個位置都有
⼀把鎖。
當向ConcurrentHashMap中put⼀個key,value時,
1. ⾸先根據key計算對應的數組下標i,如果該位置沒有元素,則通過⾃旋的⽅法去向該位置賦值。
2. 如果該位置有元素,則synchronized會加鎖
3. 加鎖成功之後,在判斷該元素的類型
a. 如果是鏈表節點則進⾏添加節點到鏈表中
b. 如果是紅⿊樹則添加節點到紅⿊樹
4. 添加成功後,判斷是否需要進⾏樹化
5. addCount,這個⽅法的意思是ConcurrentHashMap的元素個數加1,但是這個操作也是需要併發安全
的,並且元素個數加1成功後,會繼續判斷是否要進⾏擴容,如果需要,則會進⾏擴容,所以這個⽅法
很重要。
6. 同時⼀個線程在put時如果發現當前ConcurrentHashMap正在進⾏擴容則會去幫助擴容。
 
JDK7和JDK8中的ConcurrentHashMap的不同點
這兩個的不同點太多了...,既包括了HashMap中的不同點,也有其他不同點,⽐如:
1. JDK8中沒有分段鎖了,⽽是使⽤synchronized來進⾏控制
2. JDK8中的擴容性能更⾼,⽀持多線程同時擴容,實際上JDK7中也⽀持多線程擴容,因爲JDK7中的擴
容是針對每個Segment的,所以也可能多線程擴容,但是性能沒有JDK8⾼,因爲JDK8中對於任意⼀
個線程都可以去幫助擴容
3. JDK8中的元素個數統計的實現也不⼀樣了,JDK8中增加了CounterCell來幫助計數,⽽JDK7中沒
有,JDK7中是put的時候每個Segment內部計數,統計的時候是遍歷每個Segment對象加鎖統計
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章