參考自:http://www.importnew.com/28263.htmlhtml
HaspMap和ConcurrentHashMap(康科瑞特哈希邁普)java
不支持併發操做,HashMap 裏面是一個數組,而後數組中每一個元素是一個單向鏈表。node
capacity:當前數組容量,始終保持 2^n,能夠擴容,擴容後數組大小爲當前的 2 倍。數組
loadFactor:負載因子,默認爲 0.75。安全
threshold:擴容的閾值,等於 capacity * loadFactor併發
(1)當插入第一個元素的時候,須要先初始化數組大小。函數
用戶不指定容量的狀況下,默認HashMap的容量是16。若是用戶經過構造函數指定了容量,那麼HashMap會選擇大於該數字的第一個2的冪做爲容量。(3->4、7->8、9->16)。性能
(2)若是 key 爲 null,會將這個 entry(恩蠢) 放到 table[0] 中。spa
(3)若是 key 不爲 null,求 key 的 hash 值。根據 key 的哈希值找到對應的數組下標,使用 key 的 hash 值對數組長度-1進行與運算,獲得數組下標。計算方法:h & (length-1)。線程
(5)遍歷一下對應下標處的鏈表,看是否有重複的 key 已經存在,若是有,直接覆蓋,put 方法返回舊值。
(6)若是不存在重複的 key,先判斷是否須要擴容,須要的話先擴容,而後再將這個新的數據插入到擴容後的數組的相應位置處的鏈表的表頭。
在插入新值的時候,若是當前的 size 已經達到了閾值,而且要插入的數組位置上已經有元素,那麼就會觸發擴容,擴容後,數組大小爲原來的 2 倍。
擴容就是用一個新的大數組替換原來的小數組,並將原來數組中的值遷移到新的數組中。
因爲是雙倍擴容,遷移過程當中,會將原來 table[i] 中的鏈表的全部節點,分拆到新的數組的 newTable[i] 和 newTable[i + oldLength] 位置上。
例如原來數組長度是 16,那麼擴容後,原來 table[0] 處的鏈表中的全部元素會被分配到新數組中 newTable[0] 和 newTable[16] 這兩個位置。
1、若是 key 爲 null,只需遍歷下 table[0] 處的鏈表就能夠了。
1、根據 key 計算 hash 值。
2、找到相應的數組下標:hash & (length – 1)。
3、遍歷該數組位置處的鏈表裏的 entry,直到找到相等(==或equals)的 key,返回 key 對應的 value。
支持併發操做。
(1)ConcurrentHashMap 是一個 Segment(塞個門特) 數組,Segment 經過繼承 ReentrantLock 來進行加鎖,因此每次須要加鎖的操做鎖住的是一個 segment,這樣只要保證每一個 Segment 是線程安全的,也就實現了全局的線程安全。
(2)ConcurrentHashMap 有 16 個 Segments,理論上,這個時候,最多能夠同時支持 16 個線程併發寫,只要它們的操做分別分佈在不一樣的 Segment 上。這個值能夠在初始化的時候設置爲其餘值,可是一旦初始化之後,它是不能夠擴容的。
concurrencyLevel:並行級別、併發數、Segment 數,默認爲16。
initialCapacity:初始容量,這個值指的是整個 ConcurrentHashMap 的初始容量,實際操做的時候須要平均分給每一個 Segment。
loadFactor:負載因子,Segment 數組不能夠擴容,因此這個負載因子是給每一個 Segment 內部使用的。
用 new ConcurrentHashMap() 無參構造函數進行初始化的,那麼初始化完成後:
Segment 數組長度爲 16,不能夠擴容。
Segment[i] 的默認大小爲 2,負載因子是 0.75,得出初始閾值爲 1.5,也就是之後插入第一個元素不會觸發擴容,插入第二個會進行第一次擴容。
這裏初始化了 segment[0],其餘位置仍是 null。
當前 segmentShift 的值爲 32 – 4 = 28,segmentMask 爲 16 – 1 = 15,姑且把它們簡單翻譯爲移位數和掩碼。
1、計算 key 的 hash 值,根據 hash 值找到 Segment 數組中的位置 j。
hash 是 32 位,無符號右移 segmentShift(28) 位,剩下低 4 位,而後和 segmentMask(15) 作一次與操做,也就是說 j 是 hash 值的最後 4 位,也就是槽的數組下標。
2、對 segment[j] 進行初始化,
ConcurrentHashMap 初始化的時候會初始化第一個槽 segment[0],對於其餘槽來講,在插入第一個值的時候進行初始化。
對於初始化的併發操做使用 CAS 進行控制。
1. java語言CAS底層如何實現?利用unsafe(昂森福)提供的原子性操做方法。
2.什麼事ABA問題?怎麼解決?當一個值從A變成B,又更新回A,普通CAS機制會誤判經過檢測。利用版本號比較能夠有效解決ABA問題。
3、插入新值到 槽 s 中(Segment 內部的 put 操做,Segment 內部是由 數組+鏈表 組成的)
4、往該 segment 寫入前,須要先獲取該 segment 的獨佔鎖。
在往某個 segment 中 put 的時候,首先會調用 node = tryLock() ? null : scanAndLockForPut(key, hash, value),也就是說先進行一次 tryLock() 快速獲取該 segment 的獨佔鎖,若是失敗,那麼進入到 scanAndLockForPut 這個方法來獲取鎖。
segment 數組不能擴容,擴容是 segment 數組某個位置內部的數組 HashEntry[] 進行擴容,擴容後,容量爲原來的 2 倍。
首先,咱們要回顧一下觸發擴容的地方,put 的時候,若是判斷該值的插入會致使該 segment 的元素個數超過閾值,那麼先進行擴容,再插值
該方法不須要考慮併發,由於到這裏的時候,是持有該 segment 的獨佔鎖的。
1、計算 hash 值,找到 segment 數組中的具體位置
2、槽中也是一個數組,根據 hash 找到數組中具體的位置
3、到這裏是鏈表了,順着鏈表進行查找便可
添加節點的操做 put 和刪除節點的操做 remove 都是要加 segment 上的獨佔鎖的,因此它們之間天然不會有問題,咱們須要考慮的問題就是 get 的時候在同一個 segment 中發生了 put 或 remove 操做。
put 操做的線程安全性。
初始化槽,使用了 CAS 來初始化 Segment 中的數組。
添加節點到鏈表的操做是插入到表頭的,因此,若是這個時候 get 操做在鏈表遍歷的過程已經到了中間,是不會影響的。固然,另外一個併發問題就是 get 操做在 put 以後,須要保證剛剛插入表頭的節點被讀取,這個依賴於 setEntryAt 方法中使用的UNSAFE.putOrderedObject。
擴容。擴容是新建立了數組,而後進行遷移數據,最後面將 newTable 設置給屬性 table。因此,若是 get 操做此時也在進行,那麼也不要緊,若是 get 先行,那麼就是在舊的 table 上作查詢操做;而 put 先行,那麼 put 操做的可見性保證就是 table 使用了 volatile 關鍵字。
remove 操做的線程安全性。
get 操做須要遍歷鏈表,可是 remove 操做會」破壞」鏈表。
若是 remove 破壞的節點 get 操做已通過去了,那麼這裏不存在任何問題。
若是 remove 先破壞了一個節點,分兩種狀況考慮。
1、若是此節點是頭結點,那麼須要將頭結點的 next 設置爲數組該位置的元素,table 雖然使用了 volatile 修飾,可是 volatile 並不能提供數組內部操做的可見性保證,因此源碼中使用了 UNSAFE 來操做數組,請看方法 setEntryAt。
2、若是要刪除的節點不是頭結點,它會將要刪除節點的後繼節點接到前驅節點中,這裏的併發保證就是 next 屬性是 volatile 的。
(1)Java8 對 HashMap 進行了一些修改,最大的不一樣就是利用了紅黑樹,因此其由 數組+鏈表+紅黑樹 組成
(2)根據 Java7 HashMap 的介紹,咱們知道,查找的時候,根據 hash 值咱們可以快速定位到數組的具體下標,可是以後的話,須要順着鏈表一個個比較下去才能找到咱們須要的,時間複雜度取決於鏈表的長度,爲 O(n)
(3)爲了下降這部分的開銷,在 Java8 中,當鏈表中的元素超過了 8 個之後,會將鏈表轉換爲紅黑樹,在這些位置進行查找的時候能夠下降時間複雜度爲 O(logN)
(4)Java7 中使用 Entry 來表明每一個 HashMap 中的數據節點,Java8 中使用 Node,基本沒有區別,都是 key,value,hash 和 next 這四個屬性,不過,Node 只能用於鏈表的狀況,紅黑樹的狀況須要使用 TreeNode。
(5)咱們根據數組元素中,第一個節點數據類型是 Node 仍是 TreeNode 來判斷該位置下是鏈表仍是紅黑樹的。
1、第一次 put 值的時候,會觸發下面的 resize(),相似 java7 的第一次 put 也要初始化數組長度。
第一次 resize 和後續的擴容有些不同,由於此次是數組從 null 初始化到默認的 16 或自定義的初始容量。
2、找到具體的數組下標,若是此位置沒有值,那麼直接初始化一下 Node 並放置在這個位置就能夠了。
3、若是數組該位置有數據,
首先,判斷該位置的第一個數據和咱們要插入的數據,key 是否是"相等",若是是,取出這個節點。
若是該節點是表明紅黑樹的節點,調用紅黑樹的插值方法。
若是是鏈表,插入到鏈表的最後面(Java7 是插入到鏈表的最前面)
若是新插入的值是鏈表中的第 9 個,會觸發下面的 treeifyBin,也就是將鏈表轉換爲紅黑樹。
若是在該鏈表中找到了"相等"的 key(== 或 equals),此時 break,那麼 e 爲鏈表中[與要插入的新值的 key "相等"]的node
e!=null 說明存在舊值的key與要插入的key"相等",進行 "值覆蓋",而後返回舊值
4、若是 HashMap 因爲新插入這個值致使 size 已經超過了閾值,須要進行擴容。
Java7 是先擴容後插入新值的,Java8 先插值再擴容
當咱們明確知道HashMap中元素的個數的時候,把默認容量設置成 expectedSize / 0.75F + 1.0F 是一個在性能上相對好的選擇,可是,同時也會犧牲些內存
在已知HashMap中將要存放的KV個數的時候,設置一個合理的初始化容量能夠減小擴容次數,有效提升性能。
1、計算 key 的 hash 值,根據 hash 值找到對應數組下標: hash & (length-1)
2、判斷數組該第一個位置處的元素是否恰好就是咱們要找的,若是不是,走第三步
3、判斷該元素類型是不是 TreeNode,若是是,用紅黑樹的方法取數據,若是不是,走第四步
4、遍歷鏈表,直到找到相等(==或equals)的 key
結構上和 Java8 的 HashMap 基本上同樣,不過它要保證線程安全性,因此在源碼上確實要複雜一些。
經過提供初始容量,計算了 sizeCtl,sizeCtl = 【 (1.5 * initialCapacity + 1),而後向上取最近的 2 的 n 次方】。如 initialCapacity 爲 10,那麼獲得 sizeCtl 爲 16,若是 initialCapacity 爲 11,獲得 sizeCtl 爲 32。
初始化方法中的併發問題是經過對 sizeCtl 進行一個 CAS 操做來控制的
1、根據 key 計算哈希值。
2、若是數組是空的,進行初始化。
3、找該 hash 值對應的數組下標,獲得第一個節點,
4、若是數組該位置(第一個節點)爲空,用一次 CAS 操做將這個新值放入其中便可,這個 put 操做差很少就結束了。 若是 CAS 失敗,那就是有併發操做,進到下一個循環就行了。
5、若是數組該位置(第一個節點)不爲空,獲取數組該位置的頭結點的監視器鎖
頭節點的 hash 值大於 0,說明是鏈表,遍歷鏈表,若是發現了"相等"的 key,判斷是否要進行值覆蓋,而後也就能夠 break 了,若是沒有相等的,到了鏈表的最末端,將這個新值放到鏈表的最後面。
頭節點若是是紅黑樹,調用紅黑樹的插值方法插入新節點。
6、若是是鏈表,判斷是否要將鏈表轉換爲紅黑樹,臨界值和 HashMap 同樣,也是 8,這個方法和 HashMap 中稍微有一點點不一樣,那就是它不是必定會進行紅黑樹轉換,若是當前數組的長度小於 64,那麼會選擇進行數組擴容,而不是轉換爲紅黑樹
擴容也是作翻倍擴容的,擴容後數組容量爲原來的 2 倍。
這個方法的核心在於 sizeCtl 值的操做,首先將其設置爲一個負數,而後執行 transfer(tab, null),再下一個循環將 sizeCtl 加 1,並執行 transfer(tab, nt),以後多是繼續 sizeCtl 加 1,並執行 transfer(tab, nt)。
將原來的 tab 數組的元素遷移到新的 nextTab 數組中
1、計算 hash 值
2、根據 hash 值找到數組對應位置: (n – 1) & h
3根據該位置處結點性質進行相應查找
若是該位置爲 null,那麼直接返回 null 就能夠了
若是該位置處的節點恰好就是咱們須要的,返回該節點的值便可
若是該位置節點的 hash 值小於 0,說明正在擴容,或者是紅黑樹,後面咱們再介紹 find 方法
若是以上 3 條都不知足,那就是鏈表,進行遍歷比對便可