1. 程式人生 > >HashMap在多執行緒下會形成環形連結串列

HashMap在多執行緒下會形成環形連結串列

導讀:經過前面的部落格總結,可以知道的是,HashMap是有一個一維陣列和一個連結串列組成,從而得知,在解決衝突問題時,hashmap選擇的是鏈地址法。為什麼HashMap會用一個數組這連結串列組成,當時給出的答案是從那幾種解決衝突的演算法中推論的,這裡給出一個正面的理由:

1,為什麼用了一維陣列:陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);陣列的特點是:定址容易,插入和刪除困難

2,為什麼用了連結串列:連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。連結串列的特點是:定址困難,插入和刪除容易

而HashMap是兩者的結合,用一維陣列存放雜湊地址,以便更快速的遍歷;用連結串列存放地址值,以便更快的插入和刪除!

一 、環形連結串列的形成分析

那麼,在HashMap中,到底是怎樣形成環形連結串列的?這個問題,得從HashMap的resize擴容問題說起!

備註:本部落格中所示原始碼,均為java 7版本

HashMap的擴容原理:

  1. <span style="font-family:'KaiTi_GB2312';font-size:18px;"/** 
  2.      * The default initial capacity - MUST be a power of two. 
  3.      */
  4.     staticfinalint DEFAULT_INITIAL_CAPACITY = 16;  
  5.     /** 
  6.      * The maximum capacity, used if a higher value is implicitly specified 
  7.      * by either of the constructors with arguments. 
  8.      * MUST be a power of two <= 1<<30. 
  9.      */
  10.     staticfinalint MAXIMUM_CAPACITY = 1 << 30;  
  11.     /** 
  12.      * The load factor used when none specified in constructor.
     
  13.      */
  14.     staticfinalfloat DEFAULT_LOAD_FACTOR = 0.75f;</span>  

當HashMap中的元素個數超過陣列大小(陣列總大小length,不是陣列中個數size)*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為16,那麼當HashMap中元素個數超過16*0.75=12(這個值就是程式碼中的threshold值,也叫做臨界值)的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的效能。

再看原始碼中,關於擴容resize()的實現:

  1. <span style="font-family:'KaiTi_GB2312';font-size:18px;">   /** 
  2.      * Rehashes the contents of this map into a new array with a 
  3.      * larger capacity.  This method is called automatically when the 
  4.      * number of keys in this map reaches its threshold. 
  5.      * 
  6.      * If current capacity is MAXIMUM_CAPACITY, this method does not 
  7.      * resize the map, but sets threshold to Integer.MAX_VALUE. 
  8.      * This has the effect of preventing future calls. 
  9.      * 
  10.      * @param newCapacity the new capacity, MUST be a power of two; 
  11.      *        must be greater than current capacity unless current 
  12.      *        capacity is MAXIMUM_CAPACITY (in which case value 
  13.      *        is irrelevant). 
  14.      */
  15.     void resize(int newCapacity) {  
  16.         Entry[] oldTable = table;  
  17.         int oldCapacity = oldTable.length;  
  18.         if (oldCapacity == MAXIMUM_CAPACITY) {  
  19.             threshold = Integer.MAX_VALUE;  
  20.             return;  
  21.         }  
  22.         Entry[] newTable = new Entry[newCapacity];  
  23.         boolean oldAltHashing = useAltHashing;  
  24.         useAltHashing |= sun.misc.VM.isBooted() &&  
  25.                 (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  
  26.         boolean rehash = oldAltHashing ^ useAltHashing;  
  27.         transfer(newTable, rehash);  
  28.         table = newTable;  
  29.         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);  
  30.     }</span>  

備註:請注意這句話: newCapacity the new capacity,MUST be a power of two; must be greater than current capacity unless current  capacity is MAXIMUM_CAPACITY (in which case value is irrelevant)

在這裡面,又呼叫了一個函式transfer函式:

  1. <span style="font-family:'KaiTi_GB2312';font-size:18px;">   /** 
  2.      * Transfers all entries from current table to newTable. 
  3.      */
  4.     void transfer(Entry[] newTable, boolean rehash) {  
  5.         int newCapacity = newTable.length;  
  6.         for (Entry<K,V> e : table) {  
  7.             while(null != e) {  
  8.                 Entry<K,V> next = e.next;  
  9.                 if (rehash) {  
  10.                     e.hash = null == e.key ? 0 : hash(e.key);  
  11.                 }  
  12.                 int i = indexFor(e.hash, newCapacity);  
  13.                 e.next = newTable[i];  
  14.                 newTable[i] = e;  
  15.                 e = next;  
  16.             }  
  17.         }  
  18.     }</span>  
總得來說,就是拷貝舊的資料元素,從新新建一個更大容量的空間,然後進行資料複製!

那麼關於環形連結串列的形成,則主要在這擴容的過程。當多個執行緒同時對這個HashMap進行put操作,而察覺到記憶體容量不夠,需要進行擴容時,多個執行緒會同時執行resize操作,而這就出現問題了,問題的原因分析如下:

首先,在HashMap擴容時,會改變連結串列中的元素的順序,將元素從連結串列頭部插入。PS:說是為了避免尾部遍歷,這一部分不是本部落格的主要介紹內容,後面再說。

而環形連結串列就在這一時刻發生,以下模擬2個執行緒同時擴容。假設,當前hashmap的空間為2(臨界值為1),hashcode分別為0和1,在雜湊地址0處有元素A和B,這時候要新增元素C,C經過hash運算,得到雜湊地址為1,這時候由於超過了臨界值,空間不夠,需要呼叫resize方法進行擴容,那麼在多執行緒條件下,會出現條件競爭,模擬過程如下:

執行緒一:讀取到當前的hashmap情況,在準備擴容時,執行緒二介入


執行緒二:讀取hashmap,進行擴容


執行緒一:繼續執行


這個過程為,先將A複製到新的hash表中,然後接著複製B到鏈頭(A的前邊:B.next=A),本來B.next=null,到此也就結束了(跟執行緒二一樣的過程),但是,由於執行緒二擴容的原因,將B.next=A,所以,這裡繼續複製A,讓A.next=B,由此,環形連結串列出現:B.next=A; A.next=B