衆所周知,HashMap是用來存儲Key-Value鍵值對的一種集合,這個鍵值對也叫作Entry,而每一個Entry都是存儲在數組當中,所以這個數組就是HashMap的主幹。
HashMap數組中的每個元素的初始值都是NULL 算法
HaspMap的一種重要的方法是put()方法,當咱們調用put()方法時,好比hashMap.put("Java",0),此時要插入一個Key值爲「Java」的元素,這時首先須要一個Hash函數來肯定這個Entry的插入位置,設爲index,即 index = hash("Java"),假設求出的index值爲2,那麼這個Entry就會插入到數組索引爲2的位置。可是HaspMap的長度確定是有限的,當插入的Entry愈來愈多時,不一樣的Key值經過哈希函數算出來的index值確定會有衝突,此時就能夠利用鏈表來解決。
其實HaspMap數組的每個元素不止是一個Entry對象,也是一個鏈表的頭節點,每個Entry對象經過Next指針指向下一個Entry對象,這樣,當新的Entry的hash值與以前的存在衝突時,只須要插入到對應點鏈表便可。
須要注意的是,新來的Entry節點採用的是「頭插法」,而不是直接插入在鏈表的尾部,這是由於HashMap的發明者認爲,新插入的節點被查找的可能性更大。數組
get()方法用來根據Key值來查找對應點Value,當調用get()方法時,好比hashMap.get("apple"),這時一樣要對Key值作一次Hash映射,算出其對應的index值,即index = hash("apple")。
前面說到的可能存在Hash衝突,同一個位置可能存在多個Entry,這時就要從對應鏈表的頭節點開始,一個個向下查找,直到找到對應的 Key值,這樣就得到到了所要查找的鍵值對。
例如假設咱們要找的Key值是"apple": 安全
第一步,算出Key值「apple」的hash值,假設爲2。 第二步,在數組中查找索引爲2的位置,此時找到頭節點爲Entry6,Entry6的Key值是banana,不是咱們要找的值。 第三步,查找Entry6的Next節點,這裏爲Entry1,它的Key值爲apple,是咱們要查找的值,這樣就找到了對應的鍵值對,結束。微信
上面所說的就是HashMap的基本原理,能夠總結出HashMap的3個要素爲:hash函數、數組、鏈表,以下圖:接下來對於HaspMap還有不少深刻的問題,好比: 1.HashMap默認的初始長度是多少?爲何這麼規定? 2.高併發狀況下,HashMap會出現死鎖嗎? 3.Java8中,HashMap有怎樣的優化? 下面開始說明這幾個問題:多線程
1.HaspMap的默認初始長度是16,而且每次擴展長度或者手動初始化時,長度必須是2的次冪。之因此是16,是爲了服務於從Key值映射到index的hash算法。前面說到了,從Key值映射到數組中所對應的位置須要用到一個hash函數:index = hash("Java");併發
那麼爲了實現一個儘可能分佈均勻的hash函數,利用的是Key值的HashCode來作某種運算。所以問題來了,如何進行計算,才能讓這個hash函數儘可能分佈均勻呢?app
一種簡單的方法是將Key值的HashCode值與HashMap的長度進行取模運算,即 index = HashCode(Key) % hashMap.length,可是,可是!這種取模方式運算當然簡單,然而它的效率是很低的, 並且,若是使用了取模%, 那麼HashMap在容量變爲2倍時, 須要再次rehash肯定每一個鏈表元素的位置,浪費了性能。 所以爲了實現高效的hash函數算法,HashMap的發明者採用了位運算的方式。那麼如何進行位運算呢?能夠按照下面的公式:函數
index = HashCode(Key) & (hashMap.length - 1);
接下來咱們以Key值爲「apple」的例子來演示這個過程:高併發
1) 計算「apple」的hashcode,結果爲十進制的3029737,二進制的101110001110101110 1001。oop
2) HashMap默認初始長度是16,計算hashMap.Length-1的結果爲十進制的15,二進制的1111。
3) 把以上兩個結果作 與運算,101110001110101110 1001 & 1111 = 1001,十進制是9,因此 index=9。
能夠看出來,hash算法獲得的index值徹底取決與Key的HashCode的最後幾位。這樣作不但效果上等同於取模運算,並且大大提升了效率。
那麼回到最初的問題,初始長度爲何是16或者2的次冪?若是不是會怎麼樣?
咱們假設HaspMap的初始長度爲10,重複前面的運算步驟:
單獨看這個結果,表面上並無問題。咱們再來嘗試一個新的HashCode 101110001110101110 1011 :
而後咱們再換一個HashCode 101110001110101110 1111 試試 :這樣咱們能夠看到,雖然HashCode的倒數第二第三位從0變成了1,可是運算的結果都是1001。也就是說,當HashMap長度爲10的時候,有些index結果的出現概率會更大,而有些index結果永遠不會出現(好比0111)!
因此這樣顯然不符合Hash算法均勻分佈的原則。
而長度是16或者其餘2的次冪,Length – 1的值的全部二進制位全爲1(如15的二進制是1111,31的二進制爲11111),這種狀況下,index的結果就等同於HashCode後幾位的值。只要輸入的HashCode自己分佈均勻,Hash算法的結果就是均勻的。這也是HashMap設計的玄妙之處。
咱們知道HashMap是非線程安全的,那麼緣由是什麼呢?
因爲HashMap的容量是有限的,若是HashMap中的數組的容量很小,假如只有2個,那麼若是要放進10個keys的話,碰撞就會很是頻繁,此時一個O(1)的查找算法,就變成了鏈表遍歷,性能變成了O(n),這是Hash表的缺陷。
爲了解決這個問題,HashMap設計了一個閾值,其值爲容量的0.75,當HashMap所用容量超過了閾值後,就會自動擴充其容量。
在多線程的狀況下,當從新調整HashMap大小的時候,就會存在條件競爭,由於若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷。若是條件競爭發生了,那麼就會產生死循環了。
具體發生死鎖的過程能夠參考這篇文章:Java HashMap 的死循環(HashMap Infinite Loop)
最後,歡迎關注個人我的微信公衆號:業餘草(yyucao)!
本文原文出處:業餘草: » HashMap 的實現原理