1. 程式人生 > >JDK1.8 HashMap 擴容 對鏈表(長度小於默認的8)處理時重新定位的過程

JDK1.8 HashMap 擴容 對鏈表(長度小於默認的8)處理時重新定位的過程

這一 暫時 處理 滿足 有一個 java put span 條件

關於HashMap的擴容過程,請參考源碼或百度。

我想記錄的是1.8 HashMap擴容是對鏈表中節點的Hash計算分析.

對術語先明確一下:

hash計算指的確定節點在table[index]中的鏈表位置index,不是節點的hash值。

1 Node<K,V> loHead = null, loTail = null; //這兩個是記錄重新hash計算後仍在原位置(設為index)的節點
2 Node<K,V> hiHead = null, hiTail = null; //這兩個是記錄重新hash計算後在原位置加上原容量  的位置的節點(index + old capacity)

那麽問題來了 , 怎麽就確定 擴容前的 鏈表節點 在 擴容後的位置 是 當前位置或者+old capacity的位置 ?

先看 put 節點時 對key查找在table位置的計算方法,在putVal中有這麽一行(紅色部分):

1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
2                    boolean evict) {
3         Node<K,V>[] tab; Node<K,V> p; int n, i;
4         if ((tab = table) == null
|| (n = tab.length) == 0) 5 n = (tab = resize()).length; 6 if ((p = tab[i = (n - 1) & hash]) == null) 7 tab[i] = newNode(hash, key, value, null); 8 else {

其中 (n-1) & hash 就是在table中的位置(n即capacity),暫時將這個記為 oldIndex = (n-1) & hash (公式1);

再來看擴容時的代碼,代碼有點多,我刪去一些不影響理解的還是看紅色部分:

 1 final Node<K,V>[] resize() {
 2         Node<K,V>[] oldTab = table;
 3         int oldCap = (oldTab == null) ? 0 : oldTab.length;
 4         int oldThr = threshold;
 5         int newCap, newThr = 0;
 6         if (oldCap > 0) {
 7             if (oldCap >= MAXIMUM_CAPACITY) {
 8                 threshold = Integer.MAX_VALUE;
 9                 return oldTab;
10             }
11             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 
12                      oldCap >= DEFAULT_INITIAL_CAPACITY)
13                 newThr = oldThr << 1; // double threshold
14         }26         threshold = newThr;28         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
29         table = newTab;
30         if (oldTab != null) {
31             for (int j = 0; j < oldCap; ++j) {
32                 Node<K,V> e;
33                 if ((e = oldTab[j]) != null) {
34                     oldTab[j] = null;
35                     if (e.next == null)
36                         newTab[e.hash & (newCap - 1)] = e;
37                     else if (e instanceof TreeNode)
38                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
39                     else { // preserve order
40                         Node<K,V> loHead = null, loTail = null;
41                         Node<K,V> hiHead = null, hiTail = null;
42                         Node<K,V> next;
43                         do {
44                             next = e.next;
45                             if ((e.hash & oldCap) == 0) {
46                                 if (loTail == null)
47                                     loHead = e;
48                                 else
49                                     loTail.next = e;
50                                 loTail = e;
51                             }
52                             else {
53                                 if (hiTail == null)
54                                     hiHead = e;
55                                 else
56                                     hiTail.next = e;
57                                 hiTail = e;
58                             }
59                         } while ((e = next) != null);
60                         if (loTail != null) {
61                             loTail.next = null;
62                             newTab[j] = loHead;
63                         }
64                         if (hiTail != null) {
65                             hiTail.next = null;
66                             newTab[j + oldCap] = hiHead; (公式4)
67                         }
68                     }
69                 }
70             }
71         }
72         return newTab;
73     }

為了簡單(二進制可以短點)起見,以 oldCapacity = 8 為例,擴容後 capacity = 16

newCap = oldCap << 1 : 新的容量capacity= 8<<1 = 16
newTab[e.hash & (newCap - 1)] , 紅色部分即 原節點在 新table中的位置 , 暫時記為 newIndex = e.hash & (newCap - 1) (公式2)

hoHead,hoTail,hiHead,hiTail 不多說,上面有說明。 那麽 這個下面這個判斷條件,就決定了節點在擴容以後的位置

(e.hash & oldCap) == 0  (公式3)

最後再看是如何分配者兩個鏈表的,一個原位置,一個j+oldCap位置。

下面分析是如何這麽確定的:

總結上面所描述的,有下列幾個變量

oldCapacity 用 n 代替

oldIndex = (n-1) & hash (公式1)

newIndex = (n*2 -1) & hash (公式2)

condition = n & hash (公式3)

幾個值之間的關系:

if(condition == 0) 那麽 newIndex 等於 oldIndex

else newIndex 等於 oldIndex + n

裏面有一個隱含條件: n 是 2 的整數倍(也是滿足關系的必要條件)

當n = 8 時, 其二進制 b1 = 1000 , 減去1 之後(7) 得到的二進制 b2 = 0111

擴容後, 即n*2 = 16的二進制是 10000 , 減去1之後(15) 得到的二進制是 b3 = 01111

假如hash的二進制是hash = 10111 ,

  10111 & 1000 (b1) = 0 == conditon

  10111 & 0111 (b2) = 111 == oldIndex

  10111 & 01111 (b3) = 111 == newIndex

  可以發現, 當 condtion 為0 時, hash的二進制 的 第4位 必然為 0 ,高位無所謂什麽值 ,

hash &b2 (111) 結果必然是hash低三位的值,hash & b2 (1111) ,由於hash第4位為0 ,那麽結果必然仍是hash低三位的值。

  所以,當condition為0時, newIndex 必然等於 oldIndex

假如hash的二進制是hash = 11111,

    11111 & 1000 (b1) = 1000 == condition = 8

    11111 & 0111 (b2) = 00111 == oldIndex = 7

    11111 & 01111 (b3) = 01111 == newIndex = 15

     發現了嗎? oldIndex 和 newIndex 差的就是 一個第 4 位的1 ,那麽這個1 就是 2^4 = 8 , 也就是 oldCapacity (2^4) 的值。

無論多長的hash值,關鍵的一個二進制 在 第 x 位 (2^x = oldCapacity), 也就是這一位決定了 擴容前後的位置。

由於這樣計算呢, java1.8中的HashMap 可以 不用在擴容的時候 一直往頭節點插 (有環回的邏輯所以在多線程擴容的時候才會出現 閉環鏈 ),

1.8中對鏈表只有往後添加節點,沒有環回的邏輯, 也就不可能在多線程的時候出現閉環鏈。

雖然時間復雜度是一樣的,但是更機智了。

JDK1.8 HashMap 擴容 對鏈表(長度小於默認的8)處理時重新定位的過程