源碼解析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容量部分咱們就不詳細闡述,重點從數據遷移部分進行分析。咱們按照代碼順序分步進行分析。算法

一、利用for循環遍歷oldTab中的數據數組

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

二、對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) = 1this

 101atom

& 11spa


 001線程

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

   101

& 111


   101

  這種計算方式既能保證數據存儲散列,又能避免計算出的位置超出最大容量(也就是數組角標越界,由於做&運算,不會超過oldTab-1和newTab-1)。

三、鏈表轉換成了紅黑樹,針對紅黑樹的遷移方式

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);
}

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爲四、八、16...爲2的指數,其二進制爲100,1000,10000....,因此e.hash與其做&運算,假如oldCap = 4,newCap = 8,那麼最終計算獲得的值若是等於0,則該元素的位置0~3之間,除此以外在4~7之間。經過這種方式判斷元素e屬於老值仍是新值,這樣生成兩條新的鏈表。

五、生成新老鏈表後總體賦值

// 老鏈表總體賦值
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 兩個新老值來保存鏈表呢,主要是由於擴容後,鏈表中的元素的索引位置是可能發生變化的。

相關文章
相關標籤/搜索