【java基礎 12】HashMap中是如何形成環形連結串列的?
導讀:經過前面的部落格總結,可以知道的是,HashMap是有一個一維陣列和一個連結串列組成,從而得知,在解決衝突問題時,hashmap選擇的是鏈地址法。為什麼HashMap會用一個數組這連結串列組成,當時給出的答案是從那幾種解決衝突的演算法中推論的,這裡給出一個正面的理由:
1,為什麼用了一維陣列:陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);陣列的特點是:定址容易,插入和刪除困難
2,為什麼用了連結串列:連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。連結串列的特點是:定址困難,插入和刪除容易
而HashMap是兩者的結合,用一維陣列存放雜湊地址,以便更快速的遍歷;用連結串列存放地址值,以便更快的插入和刪除!
一、環形連結串列的形成分析
那麼,在HashMap中,到底是怎樣形成環形連結串列的?這個問題,得從HashMap的resize擴容問題說起!
備註:本部落格中所示原始碼,均為java 7版本
HashMap的擴容原理:
/**
* The default initial capacity - MUST be a power of two.
*/
static final int 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.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
當HashMap中的元素個數超過陣列大小(陣列總大小length,不是陣列中個數size)*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為16,那麼當HashMap中元素個數超過16*0.75=12(這個值就是程式碼中的threshold值,也叫做臨界值)的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的效能。
再看原始碼中,關於擴容resize()的實現:
/**
* 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);
}
備註:請注意這句話: 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函式:
/**
* 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;
}
}
}
總得來說,就是拷貝舊的資料元素,從新新建一個更大容量的空間,然後進行資料複製!
那麼關於環形連結串列的形成,則主要在這擴容的過程。當多個執行緒同時對這個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
二、總結
在這裡,只總結一個事兒,額,算是摘抄總結吧,就是在原始碼註釋中,發現擴容的時候,必須為2的指數,這是為什麼呢?
請點選此連結:HashMap擴容機制、執行緒安全 或者,自行學習hashmap的擴容機制
本篇部落格介紹環形連結串列的形成就先到這裡,下一篇部落格介紹怎麼判斷是否出現環形連結串列!