1. 程式人生 > >原始碼解析JDK1.8-HashMap連結串列成環的問題解決方案

原始碼解析JDK1.8-HashMap連結串列成環的問題解決方案

前言

  上篇文章詳解介紹了HashMap在JDK1.7版本中連結串列成環的原因,今天介紹下JDK1.8針對HashMap執行緒安全問題的解決方案。

jdk1.8 擴容原始碼解析

public class HashMap<K,V> extends AbstractMap<K,V>
   implements Map<K,V>, Cloneable, Serializable {
   
   // jdk1.8 HashMap擴容原始碼
final Node<K,V>[] resize() {
       
       Node<K,V>[] oldTab = table;
       
       // 到@SuppressWarnings都是計算newTab的newCap和threshold容量
       int oldCap = (oldTab == null) ? 0 : oldTab.length;
       int oldThr = threshold;
       int newCap, newThr = 0;
       if (oldCap > 0) {
           if (oldCap >= MAXIMUM_CAPACITY) {
               threshold = Integer.MAX_VALUE;
               return oldTab;
          }
           else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
               newThr = oldThr << 1; // double threshold
      }
       else if (oldThr > 0) // initial capacity was placed in threshold
           newCap = oldThr;
       else {               // zero initial threshold signifies using defaults
           newCap = DEFAULT_INITIAL_CAPACITY;
           newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
      }
       if (newThr == 0) {
           float ft = (float)newCap * loadFactor;
           newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
      }
       threshold = newThr;
       @SuppressWarnings({"rawtypes","unchecked"})
           Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
       
       // 開始進行資料遷移
       table = newTab;
       if (oldTab != null) {
           // 遍歷oldTab中的資料,並遷移到新陣列。
           for (int j = 0; j < oldCap; ++j) {
               Node<K,V> e;
               // 如果oldTab陣列中j位置資料不為null,進行遍歷,並賦值給e,避免直接對oldTab進行操作
               if ((e = oldTab[j]) != null) {
                   oldTab[j] = null;
                   // 如果oldTab的j位置資料沒有形成連結串列,就直接賦值到newTab
                   if (e.next == null)
                       newTab[e.hash & (newCap - 1)] = e;
                   // 連結串列轉換成了紅黑樹,針對紅黑樹的遷移方式
                   else if (e instanceof TreeNode)
                      ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                   // 針對連結串列的資料遷移方式
                   else { // preserve order
                       // loHead 表示老值,老值的意思是擴容後,該連結串列中計算出索引位置不變的元素
                       Node<K,V> loHead = null, loTail = null;
                       // hiHead 表示新值,新值的意思是擴容後,計算出索引位置發生變化的元素
                       Node<K,V> hiHead = null, hiTail = null;
                       Node<K,V> next;
                       do {
                           next = e.next;
                           // 表示老值連結串列,即該連結串列中計算出索引位置不變的元素
                           if ((e.hash & oldCap) == 0) {
                               if (loTail == null)
                                   loHead = e;
                               else
                                   loTail.next = e;
                               loTail = e;
                          }
                           // 表示新值連結串列,即計算出索引位置發生變化的元素
                           else {
                               if (hiTail == null)
                                   hiHead = e;
                               else
                                   hiTail.next = e;
                               hiTail = e;
                          }
                      } while ((e = next) != null);
                       // 生成連結串列後整體賦值
                       // 老連結串列整體賦值
                       if (loTail != null) {
                           loTail.next = null;
                           newTab[j] = loHead;
                      }
                       // 新連結串列整體賦值
                       if (hiTail != null) {
                           hiTail.next = null;
                           newTab[j + oldCap] = hiHead;
                      }
                  }
              }
          }
      }
       return newTab;
  }
}

  看完上邊的程式碼後,可能也是一頭霧水,下面我們重點對其中的細節點進行解析,針對計算newTab的newCap和threshold容量部分我們就不詳細闡述,重點從資料遷移部分進行分析。我們按照程式碼順序分步進行分析。

1、利用for迴圈遍歷oldTab中的資料

for (int j = 0; j < oldCap; ++j) {
  Node<K,V> e;

2、對oldTab在j位置的資料進行判斷,並進行資料遷移操作

  如果在oldTab的j位置資料沒有形成連結串列

if (e.next == null)
  newTab[e.hash & (newCap - 1)] = e;

  如果e.next == null,也就是e沒有next資料節點,通過這種方法判斷是否形成了連結串列資料結構,如果沒有形成連結串列資料結構,直接將資料放到對應newTab的位置即可。

e.hash & (newCap - 1) : 代表newTab存放e資料的位置,假如,e.hash = 5,oldCap = 4(老陣列oldTab的長度),newCap = 8(新陣列newTab的長度,即老陣列2倍擴容),那麼該e元素:在oldTab中的位置為:5 & (4 - 1) = 1

 101

& 11


 001

在newTab中的位置為:5 & (8 - 1) = 5

   101

& 111


   101

  這種計算方式既能保證資料儲存雜湊,又能避免計算出的位置超出最大容量(也就是陣列角標越界,因為作&運算,不會超過oldTab-1和newTab-1)。

3、連結串列轉換成了紅黑樹,針對紅黑樹的遷移方式

else if (e instanceof TreeNode)
  ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

  具體的遍歷方式贊不做解析,如想了解更多,請關注公眾號“程式設計師清辭”。

4、針對連結串列的資料遷移方式

else { // preserve order
  // loHead 表示老值,老值的意思是擴容後,該連結串列中計算出索引位置不變的元素
  Node<K,V> loHead = null, loTail = null;
  // hiHead 表示新值,新值的意思是擴容後,計算出索引位置發生變化的元素
  Node<K,V> hiHead = null, hiTail = null;
  Node<K,V> next;
  do {
    next = e.next;
    // 表示老值連結串列,即該連結串列中計算出索引位置不變的元素
    if ((e.hash & oldCap) == 0) {
      if (loTail == null)
        loHead = e;
      else
        loTail.next = e;
      loTail = e;
    }
    // 表示新值連結串列,即計算出索引位置發生變化的元素
    else {
      if (hiTail == null)
        hiHead = e;
      else
        hiTail.next = e;
      hiTail = e;
    }
  } while ((e = next) != null);
}

jdk1.8版本為了提高連結串列遷移的效率,引用兩個新的概念:

loHead:表示老值,老值的意思是擴容後,該連結串列中計算出索引位置不變的元素。

hiHead:表示新值,新值的意思是擴容後,計算出索引位置發生變化的元素。

  舉個例子,陣列大小是 8 ,在陣列索引位置是 1 的地方掛著一個連結串列,連結串列有兩個值,兩個值的 hashcode 分別是是9和33。當陣列發生擴容時,新陣列的大小是 16,此時 hashcode 是 33 的值計算出來的陣列索引位置仍然是 1,我們稱為老值hashcode 是 9 的值計算出來的陣列索引位置是 9,就發生了變化,我們稱為新值。

  針對連結串列做do-while遍歷,條件為(e = next) != null。利用(e.hash & oldCap) == 0來判斷元素e屬於新值連結串列還是老值連結串列。參考上面索引位置計算演算法 e.hash & (oldCap - 1),這次直接利用e.hash與oldCap作&運算,因為oldCap為4、8、16...為2的指數,其二進位制為100,1000,10000....,所以e.hash與其作&運算,假如oldCap = 4,newCap = 8,那麼最終計算得到的值如果等於0,則該元素的位置0~3之間,除此之外在4~7之間。通過這種方式判斷元素e屬於老值還是新值,這樣生成兩條新的連結串列。

5、生成新老連結串列後整體賦值

// 老連結串列整體賦值
if (loTail != null) {
  loTail.next = null;
  newTab[j] = loHead;
}
// 新連結串列整體賦值
if (hiTail != null) {
  hiTail.next = null;
  newTab[j + oldCap] = hiHead;
}

  如果是老連結串列,直接將資料賦值給newTab[j]。如果是新連結串列,需要進行將j增加oldCap的長度,通過e.hash & (newTab - 1)計算後得到的也是這個值。計算原理是相通的。

總結:

  1. jdk1.8 是等連結串列整個 while 迴圈結束後,才給陣列賦值,此時使用區域性變數 loHead 和 hiHead 來儲存連結串列的值,因為是區域性變數,所以多執行緒的情況下,肯定是沒有問題的。

  2. 為什麼有 loHead 和 hiHead 兩個新老值來儲存連結串列呢,主要是因為擴容後,連結串列中的元素的索引位置是可能發生變化的。