HashMap在多執行緒下會形成環形連結串列
導讀:經過前面的部落格總結,可以知道的是,HashMap是有一個一維陣列和一個連結串列組成,從而得知,在解決衝突問題時,hashmap選擇的是鏈地址法。為什麼HashMap會用一個數組這連結串列組成,當時給出的答案是從那幾種解決衝突的演算法中推論的,這裡給出一個正面的理由:
1,為什麼用了一維陣列:陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);陣列的特點是:定址容易,插入和刪除困難
2,為什麼用了連結串列:連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。連結串列的特點是:定址困難,插入和刪除容易
而HashMap是兩者的結合,用一維陣列存放雜湊地址,以便更快速的遍歷;用連結串列存放地址值,以便更快的插入和刪除!
一 、環形連結串列的形成分析
那麼,在HashMap中,到底是怎樣形成環形連結串列的?這個問題,得從HashMap的resize擴容問題說起!
備註:本部落格中所示原始碼,均為java 7版本
HashMap的擴容原理:
- <span style="font-family:'KaiTi_GB2312';font-size:18px;"> /**
- * The default initial capacity - MUST be a power of two.
- */
- staticfinalint DEFAULT_INITIAL_CAPACITY = 16;
- /**
- * The maximum capacity, used if a higher value is implicitly specified
- * by either of the constructors with arguments.
- * MUST be a power of two <= 1<<30.
- */
- staticfinalint MAXIMUM_CAPACITY = 1 << 30;
- /**
- * The load factor used when none specified in constructor.
- */
- 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()的實現:
- <span style="font-family:'KaiTi_GB2312';font-size:18px;"> /**
- * Rehashes the contents of this map into a new array with a
- * larger capacity. This method is called automatically when the
- * number of keys in this map reaches its threshold.
- *
- * If current capacity is MAXIMUM_CAPACITY, this method does not
- * resize the map, but sets threshold to Integer.MAX_VALUE.
- * This has the effect of preventing future calls.
- *
- * @param 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).
- */
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
- Entry[] newTable = new Entry[newCapacity];
- boolean oldAltHashing = useAltHashing;
- useAltHashing |= sun.misc.VM.isBooted() &&
- (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
- boolean rehash = oldAltHashing ^ useAltHashing;
- transfer(newTable, rehash);
- table = newTable;
- threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
- }</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函式:
- <span style="font-family:'KaiTi_GB2312';font-size:18px;"> /**
- * Transfers all entries from current table to newTable.
- */
- void transfer(Entry[] newTable, boolean rehash) {
- int newCapacity = newTable.length;
- for (Entry<K,V> e : table) {
- while(null != e) {
- Entry<K,V> next = e.next;
- if (rehash) {
- e.hash = null == e.key ? 0 : hash(e.key);
- }
- int i = indexFor(e.hash, newCapacity);
- e.next = newTable[i];
- newTable[i] = e;
- e = next;
- }
- }
- }</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