上篇:《對於HashMap,你知道多少?》

上篇:《對於HashMap,你知道多少?》

 

閱讀目錄面試

  • 1、前言
  • 2、源碼解讀
  • 3、併發場景中使用HashMap會怎麼樣?
  • 4、怎樣合理使用HashMap?

1、前言

HashMap在面試中是個火熱的話題,那麼你能應付自如嗎?下面拋出幾個問題看你是否知道,若是知道那麼本文對於你來講就不值一提了。算法

  • HashMap的內部數據結構是什麼?
  • HashMap擴容機制時什麼?何時擴容?
  • HashMap其長度有什麼特徵?爲何是這樣?
  • HashMap爲何線程不安全?併發的場景會出現什麼的狀況?

本文是基於JDK1.7.0_79版本進行研究的。數組

2、源碼解讀

一、類的繼承關係安全

上篇:《對於HashMap,你知道多少?》

 

其中繼承了AbstractMap抽象類,別小看了這個抽象類哦,它實現了Map接口的許多重要方法,大大減小了實現此接口的工做量。性能優化

二、屬性解析數據結構

2.一、capacity:容量架構

  • DEFAULT_INITIAL_CAPACITY:默認的初始容量-必須是2的冪。爲何呢?先留個疑問在這

上篇:《對於HashMap,你知道多少?》

 

  • MAXIMUM_CAPACITY:最大容量爲2^30。

2.2 threshold:閾值併發

上篇:《對於HashMap,你知道多少?》

 

從上面註釋能夠看出, 它的值是由容量和加載因子決定的。分佈式

2.3 loadFactor:加載因子,默認爲0.75函數

上篇:《對於HashMap,你知道多少?》

 

2.4 size:鍵值對長度

上篇:《對於HashMap,你知道多少?》

 

2.5 modCount:修改內部結構的次數

上篇:《對於HashMap,你知道多少?》

 

上面五個屬性字段都很重要, 後面再分析體現其重要。

三、底層數據結構

上篇:《對於HashMap,你知道多少?》

 

Entry內部結構以下:

上篇:《對於HashMap,你知道多少?》

 

經分析後其數據結構爲數組+鏈表的形式,展現圖以下:

上篇:《對於HashMap,你知道多少?》

 

四、重要函數

4.1 構造函數

總共有四個構造函數, 主要分析含有兩個參數的構造函數:

上篇:《對於HashMap,你知道多少?》

 

其實這個構造函數也主要是初始化加載因子和閾值。(可能1.7的其餘版本會有點不同,會在構造函數中初始化table)

上篇:《對於HashMap,你知道多少?》

 

4.2 put()函數

上篇:《對於HashMap,你知道多少?》

 

  • 第一步:當table尚未初始化時,看下inflateTable()函數作了什麼操做。

上篇:《對於HashMap,你知道多少?》

 

  • 其中容量是根據toSize取第一個大於它的2的指數次冪的值, 以下,其中highestOneBit函數是返回其最高位的權值,用的最巧的就是(number - 1) << 1 其實就是取number的倍數, 但綜合使用卻能取得第一個大於等於該值的2的指數次冪。(用的牛逼)

上篇:《對於HashMap,你知道多少?》

 

  • 接着看put函數的第二步:當key爲null時,會取數組下標爲0的位置進行鏈表遍歷,若是存在key=null,則替換值並返回。不然進入第六步(注意:索引值依然指定是0)。

上篇:《對於HashMap,你知道多少?》

 

  • 第三步:根據key的hashCode求取hash值,這又是個神奇的算法,這裏不作多解釋。

上篇:《對於HashMap,你知道多少?》

 

  • 第四步:根據hash值和底層數組的長度計算索引下標。由於數組的長度是2的冪,因此h & (length-1)運算其實就是h與(length-1)的取模運算。不得不服啊,將計算運用的如此高效。

上篇:《對於HashMap,你知道多少?》

 

找個數驗證下:

上篇:《對於HashMap,你知道多少?》

 

  • 第五步是驗證是否有重複key,若是有則替換新值而後返回,源碼很詳細了就再也不作解釋了。
  • 第六步:是將值添加到entry數組中,詳細看下addEntry()函數。首先根據size和閾值判斷是否須要擴容(進行兩倍擴容),若是須要擴容則先擴容從新計算索引,則建立新的元素添加至數組。

上篇:《對於HashMap,你知道多少?》

 

其中擴容機制resize()函數須要重點撈出來曬下:newCapacity = 2 * length,理論上會進行兩倍擴容但會根最大容量進行對比取最小, 建立新數組而後將就數組中的值拷貝至新數組(其中會從新計算索引下標),而後再賦值給table, 最後再從新計算閾值。

上篇:《對於HashMap,你知道多少?》

 

接着看transfer()函數,多注意這個函數中循環的內容

上篇:《對於HashMap,你知道多少?》

 

經過上面分析,其實put函數仍是簡單的,不是很繞。那麼能從其中找到開頭的第二和第三個問題的答案嗎?下面總結下順便回答下這兩個問題:

一、數組長度不論是初始化仍是擴容時,都始終保持是2的指數次冪。爲何呢?下面個人分析:

  • 能使元素均勻分佈,增大空間利用率。put值時須要根據key的hash值與長度進行取模運算獲得索引下標,若是是2的冪,那麼length必定是偶數,則length-1必定是奇數,那麼它對應的二進制的最後一位必定是1,因此它能保證h&(length-1)既能到奇數也能獲得偶數,這樣保證了散列的均勻性。相反若是不是2的冪,那麼length-1多是偶數,這樣h&(length-1)獲得的都是偶數,就會浪費一半的空間了。
  • 運算效率高效。位運算比%運算高效。

二、重複key的值會被新值替換,容許key爲空且統一放在下標爲0的鏈表上。

三、當size大於等於閾值(容量*加載因子)時,會進行擴容。擴容機制是:擴容量爲原來數組長度的兩倍,根據擴容量建立新數組而後進行數組拷貝,新元素落位須要從新計算索引下標。擴容後,閾值須要從新計算,須要插入的元素落位的索引下標也須要從新計算。

四、擴容很耗時,而擴容的次數主要取決於加載因子的值,由於它決定這擴容的次數。下面講下它的取值的重要性:

  • 加載因子越小,優勢:存儲的衝突機會減小;缺點:擴容次數越多(消耗性能就越大)、同時浪費空間較大(不少空間還沒用,就開始擴容了)
  • 加載因子越大,有點:擴容次數較少,空間利用率高;缺點:衝突概率就變大了、鏈表(後面介紹)長度會變長,查找的效率下降

五、擴容時會從新計算索引下標。也就是所謂的rehash過程。

六、插入元素都是表頭插入,而不是鏈表尾插入。

4.三、get()函數

知道了put方法的原理,那麼get方法就很簡單了。

上篇:《對於HashMap,你知道多少?》

 

第一步:若是key爲空,則直接從table[0]所對應的鏈表中查找(應該還記得put的時候爲null的key放在哪)。

上篇:《對於HashMap,你知道多少?》

 

第二步:若是key不爲空,則根據key獲取hash值,而後再根據hash和length-1取模獲得索引,而後再遍歷索引對應的鏈表,存在與key相等的則返回。

上篇:《對於HashMap,你知道多少?》

 

3、併發場景中使用HashMap會怎麼樣?

一、確定不能保證數據的安全性,由於內部方法沒有一個是線程安全的。

二、有時會出現死鎖狀況。爲何呢?下面列個場景簡單分析下:

  • 假設當前容量爲4, 有三個元素(a, b, c)都在table[2]下的鏈表中,另外一個元素(d)在table[3]下。如圖

上篇:《對於HashMap,你知道多少?》

 

  • 假設此時有A,B兩個線程都要往map中put一個元素則都須要擴容,當遍歷到table[2]時,假設線程B先進入循環體的第一步:e 指向a, next指向b, 如圖:

上篇:《對於HashMap,你知道多少?》

 

上篇:《對於HashMap,你知道多少?》

 

  • 此時線程B讓出時間片,讓A線程一直執行完擴容操做,最終落位一樣也是落位到table[2],其鏈表元素已經倒序了。如圖:

上篇:《對於HashMap,你知道多少?》

 

  • A線程讓出時間片,B線程操做:接着循環繼續執行,執行到循環末尾的時候,table[2] 指向a, 同時 e 和 next 都是指向b,如圖:

上篇:《對於HashMap,你知道多少?》

 

上篇:《對於HashMap,你知道多少?》

 

  • 接着第二輪循環, e = b, next = a, 進行第二輪循環後的結果是e = next 且 table[2] 指向b元素,b元素再指向a元素,如圖:

上篇:《對於HashMap,你知道多少?》

 

  • 接着第三輪循環, e = a, a的下個元素爲null, 因此next = null,可是當執行到下面這步就改變形式了,e.next 又指向了b,此時a和b已經出現了環形。由於next = null,因此終止了循環。

上篇:《對於HashMap,你知道多少?》

 

上篇:《對於HashMap,你知道多少?》

 

  • 此時,問題尚未直接產生。當調用get()函數查找一個不存在的Key,而這個Key的Hash結果剛好等於3的時候,因爲位置3帶有環形鏈表,因此程序將會進入死循環!(上面圖形均忽略四個元素和要插入元素的規劃)
  • 注:歡迎工做1到5年的Java工程師朋友們加入Java高級交流:468897908。羣內提供免費的Java架構學習資料(有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化等...)這些成爲架構師必備的知識體系,以及Java進階學習路線圖。

4、怎樣合理使用HashMap?

  • 一、建立HashMap時,指定足夠大的容量,減小擴容次數。最好爲:須要存的實際個數/除以加載因子。可使用guava包中的Maps.newHashMapWithExpectedSize()方法。

爲何要這樣指定大小呢? 再去上面回顧下擴容時機吧

  • 二、不要在併發場景中使用HashMap,如硬要使用經過Collections工具類建立線程安全的map,如:Collections.synchronizedMap(new HashMap<String, Object>());
相關文章
相關標籤/搜索