閱讀目錄面試
- 1、前言
- 2、源碼解讀
- 3、併發場景中使用HashMap會怎麼樣?
- 4、怎樣合理使用HashMap?
1、前言
HashMap在面試中是個火熱的話題,那麼你能應付自如嗎?下面拋出幾個問題看你是否知道,若是知道那麼本文對於你來講就不值一提了。算法
- HashMap的內部數據結構是什麼?
- HashMap擴容機制時什麼?何時擴容?
- HashMap其長度有什麼特徵?爲何是這樣?
- HashMap爲何線程不安全?併發的場景會出現什麼的狀況?
本文是基於JDK1.7.0_79版本進行研究的。數組
2、源碼解讀
一、類的繼承關係安全
其中繼承了AbstractMap抽象類,別小看了這個抽象類哦,它實現了Map接口的許多重要方法,大大減小了實現此接口的工做量。性能優化
二、屬性解析數據結構
2.一、capacity:容量架構
- DEFAULT_INITIAL_CAPACITY:默認的初始容量-必須是2的冪。爲何呢?先留個疑問在這
- MAXIMUM_CAPACITY:最大容量爲2^30。
2.2 threshold:閾值併發
從上面註釋能夠看出, 它的值是由容量和加載因子決定的。分佈式
2.3 loadFactor:加載因子,默認爲0.75函數
2.4 size:鍵值對長度
2.5 modCount:修改內部結構的次數
上面五個屬性字段都很重要, 後面再分析體現其重要。
三、底層數據結構
Entry內部結構以下:
經分析後其數據結構爲數組+鏈表的形式,展現圖以下:
四、重要函數
4.1 構造函數
總共有四個構造函數, 主要分析含有兩個參數的構造函數:
其實這個構造函數也主要是初始化加載因子和閾值。(可能1.7的其餘版本會有點不同,會在構造函數中初始化table)
4.2 put()函數
- 第一步:當table尚未初始化時,看下inflateTable()函數作了什麼操做。
- 其中容量是根據toSize取第一個大於它的2的指數次冪的值, 以下,其中highestOneBit函數是返回其最高位的權值,用的最巧的就是(number - 1) << 1 其實就是取number的倍數, 但綜合使用卻能取得第一個大於等於該值的2的指數次冪。(用的牛逼)
- 接着看put函數的第二步:當key爲null時,會取數組下標爲0的位置進行鏈表遍歷,若是存在key=null,則替換值並返回。不然進入第六步(注意:索引值依然指定是0)。
- 第三步:根據key的hashCode求取hash值,這又是個神奇的算法,這裏不作多解釋。
- 第四步:根據hash值和底層數組的長度計算索引下標。由於數組的長度是2的冪,因此h & (length-1)運算其實就是h與(length-1)的取模運算。不得不服啊,將計算運用的如此高效。
找個數驗證下:
- 第五步是驗證是否有重複key,若是有則替換新值而後返回,源碼很詳細了就再也不作解釋了。
- 第六步:是將值添加到entry數組中,詳細看下addEntry()函數。首先根據size和閾值判斷是否須要擴容(進行兩倍擴容),若是須要擴容則先擴容從新計算索引,則建立新的元素添加至數組。
其中擴容機制resize()函數須要重點撈出來曬下:newCapacity = 2 * length,理論上會進行兩倍擴容但會根最大容量進行對比取最小, 建立新數組而後將就數組中的值拷貝至新數組(其中會從新計算索引下標),而後再賦值給table, 最後再從新計算閾值。
接着看transfer()函數,多注意這個函數中循環的內容
經過上面分析,其實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方法就很簡單了。
第一步:若是key爲空,則直接從table[0]所對應的鏈表中查找(應該還記得put的時候爲null的key放在哪)。
第二步:若是key不爲空,則根據key獲取hash值,而後再根據hash和length-1取模獲得索引,而後再遍歷索引對應的鏈表,存在與key相等的則返回。
3、併發場景中使用HashMap會怎麼樣?
一、確定不能保證數據的安全性,由於內部方法沒有一個是線程安全的。
二、有時會出現死鎖狀況。爲何呢?下面列個場景簡單分析下:
- 假設當前容量爲4, 有三個元素(a, b, c)都在table[2]下的鏈表中,另外一個元素(d)在table[3]下。如圖
- 假設此時有A,B兩個線程都要往map中put一個元素則都須要擴容,當遍歷到table[2]時,假設線程B先進入循環體的第一步:e 指向a, next指向b, 如圖:
- 此時線程B讓出時間片,讓A線程一直執行完擴容操做,最終落位一樣也是落位到table[2],其鏈表元素已經倒序了。如圖:
- A線程讓出時間片,B線程操做:接着循環繼續執行,執行到循環末尾的時候,table[2] 指向a, 同時 e 和 next 都是指向b,如圖:
- 接着第二輪循環, e = b, next = a, 進行第二輪循環後的結果是e = next 且 table[2] 指向b元素,b元素再指向a元素,如圖:
- 接着第三輪循環, e = a, a的下個元素爲null, 因此next = null,可是當執行到下面這步就改變形式了,e.next 又指向了b,此時a和b已經出現了環形。由於next = null,因此終止了循環。
- 此時,問題尚未直接產生。當調用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>());