在JDK1.8之前版本中,HashMap的實現是數組+鏈表,它的缺點是即便哈希函數選擇的再好,也很難達到元素百分百均勻分佈,並且當HashMap中有大量元素都存到同一個桶中時,這個桶會有一個很長的鏈表,此時遍歷的時間複雜度就是O(n),固然這是最糟糕的狀況。node
在JDK1.8及之後的版本中引入了紅黑樹結構,HashMap的實現就變成了數組+鏈表或數組+紅黑樹。添加元素時,若桶中鏈表個數超過8,鏈表會轉換成紅黑樹;刪除元素、擴容時,若桶中結構爲紅黑樹而且樹中元素個數較少時會進行修剪或直接還原成鏈表結構,以提升後續操做性能;遍歷、查找時,因爲使用紅黑樹結構,紅黑樹遍歷的時間複雜度爲 O(logn),因此性能獲得提高。面試
HashMap在JDK1.8及之後的版本中引入了紅黑樹結構,若桶中鏈表元素個數大於等於8時,鏈表轉換成樹結構;若桶中鏈表元素個數小於等於6時,樹結構還原成鏈表。由於紅黑樹的平均查找長度是log(n),長度爲8的時候,平均查找長度爲3,若是繼續使用鏈表,平均查找長度爲8/2=4,這纔有轉換爲樹的必要。鏈表長度若是是小於等於6,6/2=3,雖然速度也很快的,可是轉化爲樹結構和生成樹的時間並不會過短。算法
選擇6和8,中間有個差值7能夠有效防止鏈表和樹頻繁轉換。假設一下,若是設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,若是一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。數組
在JDK1.8以前的版本中,HashMap的底層實現是數組+鏈表。當調用HashMap的put方法添加元素時,若是新元素的hash值或key在原Map中不存在,會檢查容量size有沒有超過設定的threshold,若是超過則須要進行擴容,擴容的容量是原數組的兩倍,具體代碼以下:數據結構
void addEntry(int hash, K key, V value, int bucketIndex) { //檢查容量是否超過threshold if ((size >= threshold) && (null != table[bucketIndex])) { //擴容 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
擴容就是新建Entry數組,並將原Map中元素從新計算hash值,而後存到新數組中,具體代碼以下:架構
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]; //原數組元素轉存到新數組中 transfer(newTable, initHashSeedAsNeeded(newCapacity)); //指向新數組 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } 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的初始容量是4,使用默認負載因子0.75,有三個元素經過Hash算法計算出的數組下標都是2,可是key值都不一樣,分別是a一、a二、a3,HashMap內部存儲以下圖:less
假設插入的第四個元素a4,經過Hash算法計算出的數組下標也是2,當插入時則須要擴容,此時有兩個線程T一、T2同時插入a4,則T一、T2同時進行擴容操做,它們各自新建了一個Entry數組newTable。dom
T2線程執行到transfer方法的Entry<K,V> next = e.next;時被掛起,T1線程執行transfer方法後Entry數組以下圖:函數
在T1線程沒返回新建Entry數組以前,T2線程恢復,由於在T2掛起時,變量e指向的是a1,變量next指向的是a2,因此在T2恢復執行完transfer以後,Entry數組以下圖:在此我向你們推薦一個架構學習交流裙。交流學習裙號:821169538,裏面會分享一些資深架構師錄製的視頻錄像 性能
能夠看到在T2執行完transfer方法後,a1元素和a2元素造成了循環引用,此時不管將T1的Entry數組仍是T2的Entry數組返回做爲擴容後的新數組,都會存在這個環形鏈表,當調用get方法獲取該位置的元素時就會發生死循環,更嚴重會致使CPU佔用100%故障。
JDK8中HashMap擴容涉及到的加載因子和鏈表轉紅黑樹的知識點常常被做爲面試問答題,下面對這兩個知識點進行小結。
在JDK8及之後的版本中,HashMap引入了紅黑樹結構,其底層的數據結構變成了數組+鏈表或數組+紅黑樹。添加元素時,若桶中鏈表個數超過8,鏈表會轉換成紅黑樹。以前有寫過篇幅分析選擇數字8的緣由,內容不夠嚴謹。最近從新翻了一下HashMap的源碼,發現其源碼中有這樣一段註釋:
Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFYTHRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poissondistribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-pow(0.5, k) / factorial(k)). The first values are: 0: 0.60653066 1: 0.30326533 2: 0.07581633 3: 0.01263606 4: 0.00157952 5: 0.00015795 6: 0.00001316 7: 0.00000094 8: 0.00000006 more: less than 1 in ten million
翻譯過來大概的意思是:理想狀況下使用隨機的哈希碼,容器中節點分佈在hash桶中的頻率遵循泊松分佈,具體能夠查看泊松分佈,按照泊松分佈的計算公式計算出了桶中元素個數和機率的對照表,能夠看到鏈表中元素個數爲8時的機率已經很是小,再多的就更少了,因此原做者在選擇鏈表元素個數時選擇了8,是根據機率統計而選擇的。
HashMap有兩個參數影響其性能:初始容量和加載因子。容量是哈希表中桶的數量,初始容量只是哈希表在建立時的容量。加載因子是哈希表在其容量自動擴容以前能夠達到多滿的一種度量。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行擴容、rehash操做(即重建內部數據結構),擴容後的哈希表將具備兩倍的原容量。
一般,加載因子須要在時間和空間成本上尋求一種折衷。加載因子太高,例如爲1,雖然減小了空間開銷,提升了空間利用率,但同時也增長了查詢時間成本;加載因子太低,例如0.5,雖然能夠減小查詢時間成本,可是空間利用率很低,同時提升了rehash操做的次數。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小rehash操做次數,因此,通常在使用HashMap時建議根據預估值設置初始容量,減小擴容操做。
選擇0.75做爲默認的加載因子,徹底是時間和空間成本上尋求的一種折衷選擇,至於爲何不選擇0.5或0.8,筆者沒有找到官方的直接說明,在HashMap的源碼註釋中也只是說是一種折中的選擇。