JDK8-HashMap源碼分析

HashMap vs HashTablehtml

         HashTable若是插入key/value爲null的值時,會報錯,可是hashmap不會,在hashmap中,null是做爲第0個元素的,至關因而作了特殊化處理。算法

         前者是非線程安全的,後者是線程安全的. 後者線程安全的緣由就是由於後者的每個方法上都有一個synchronized,這樣雖然保障了線程安全,可是每次都要鎖整個class對象,而且還會阻止其餘synchronize方法的訪問,因此效率低下!  HashTable已經廢棄了,儘可能別使用了.shell

         因此綜上所述,HashTable效率低的緣由是由於全部訪問它的線程都必須競爭同一把鎖,若是容器裏面不一樣的數據有多把鎖,那麼執行的效率就高了,因此ConcurrentHashMap使用的就是鎖分段的技術.數據庫

 

JDK1.8以後,HashMap和以前1.7不一樣的是,再也不單純的由數組+鏈表的方式實現的(所謂的鏈地址法).  1.7因爲在Hash衝突的時候,在桶上造成的鏈表會愈來愈長,這樣在查詢的時候效率就會變低,1.8以後改爲了由紅黑樹實現.即:數組

 

 

因此,JAVA8中的HashMap是由: 數組+鏈表/紅黑樹組成安全

 

         Java7使用Entry來表示每一個HashMap中的數據節點,8中使用了Node,基本沒什麼區別,都是key,value,hash和next這四個屬性來修飾鏈表,而紅黑樹的狀況須要使用TreeNode函數

 

類屬性分析:

默認容量爲: 16性能

 

 

         這裏的默認容量爲何是2的n次冪?.net

         查看它的put方法可知,key在Node[]中的下標爲: (n - 1) & hash。若是這個n是2的N次冪,那麼hash至關於和111***111作與運算,數據分散的就比較均勻。若是n不是在的N次冪,即hash有可能和1110作與運算,那麼最後一位怎麼與都是0,那至關於結尾是1的那幾個下標永遠都不會放數據了,好比0001,0011…這確定會增長碰撞的概率.線程

         若是size > capacity*loadFactor的話,hashmap還會進行resize操做,會至關耗性能!因此若是事先能夠肯定你要放進hashMap中的數據大小。那麼應該儘可能設置成loadFactor * 2^n >  initCapacity ,這樣既考慮了&的問題,也避免了resize的問題.

         可是後面提到會有tableSizeFor()和在put的時候考慮loadFact來保證上面這兩個要求的.因此在初始化的時候,設置成本身知道的大小便可,衝突這些由hashmap自身來幫你減免!

         不對,仍是得本身算下,若是你輸入的initcapacity爲7,那麼算出來的最近的2^n爲8,若是選擇的是默認的0.75,即最多放入6個元素就要擴容,你放到第7個的時候,仍是要resize()…

 

最大的capacity爲 2^30

 

 

 

默認負載因子爲0,75,即size > capacity * 0.75,hashmap就要進行擴容

 

 

特殊狀況下:

1)內存空間不少,時間效率要求很高,能夠下降loadFactor(儘可能讓table擴寬)

2)若是內存緊張,時間效率要求不高,能夠增長loadFactor

 

一個桶中,bin(箱子)的存儲方式由鏈表轉成紅黑樹的閾值爲8

 

 

一個桶中,由紅黑樹轉成鏈表的閾值,resize的時候可能會用到這個值.

 

 

 

當桶中的bin被樹化時最小的hash表容量.若是樹化時bin的數量太多會進行resize擴容.註釋中說MIN_TREEIFY_CAPACITY至少是 4 * TREEIFY_THRESHOLD

 

 

 

上面說了這麼多的bin,這裏該介紹下bin究竟是個什麼結構了.

每一個bin在HashMap表明存儲了一個K/V鍵值對,結構定義以下:

 

 

 

 

 

Hashmap中計算key的hash值

 

 

能夠看到它並無直接使用Object中生成hashcode的方法,這個方法叫擾動函數,和以前的要將capacity設計成2^n同樣,也是爲了減小碰撞用的.

根據前面可知,key在Node[]中的下標爲 key.hashCode & (n-1),咱們知道key.hashCode是一個很長的int類型的數字(範圍大概40億),而n-1顯然沒有這麼長,若是直相與,那麼只有key.hashCode的後面幾位參與運算了,顯然會使得碰撞很激烈!加了這個函數以後,讓高位也想辦法參與到運算中來,這樣就有可能進一步下降碰撞的可能性了!

 

用於存儲Node(K/V)對的hash表(數組),爲何是transient?

 

 

爲了解答這個問題,咱們須要明確下面事實:

Object.hashCode方法對於一個類的兩個實例返回的是不一樣的哈希值

能夠試想下面的場景:

咱們在機器A上算出對象A的哈希值與索引,而後把它插入到HashMap中,而後把該HashMap序列化後,在機器B上從新算對象的哈希值與索引,這與機器A上算出的是不同的,因此咱們在機器B上get對象A時,會獲得錯誤的結果。

因此說,當序列化一個HashMap對象時,保存Entry的table是不須要序列化進來的,由於它在另外一臺機器上是錯誤的,因此屬性這裏爲transient。

由於這個緣由,HashMap重寫了writeObject與readObject 方法

 

保存K/V對的Set

 

 

目前hashMap中K/V對的數量

 

 

 

每次對這個hashmap作操做,這個modCount就會改變.(CAS?!)

 

 

 

Threadhold表示當容量達到該值時,會進行resize

loadFactor表示用戶設置的負載因子大小

 

 

 

 

構造函數:

         有三種,一種是無參的,這個沒啥好看的,一種是隻配置了initialCapacity,最後一種是設置了initcapacity和loadFactor。看第三種就夠了,第二種不過是將loadFactor這個形參用默認0.75傳入而已.

 

 

方法內部是將loadFactor這個屬性設置爲用戶輸入的大小,有意思的是tableSizeFor(initCap)這個函數,也就是說你輸入一個10,hash表的大小不必定就是10. 這個函數的功能就是用來保證容量應該大於cap,且爲2的整數冪.

 

隨便寫個數帶進去算一下就可知道,該算法的做用讓最高位的1後面的位全變爲1.而後再+1,獲得的就是恰巧的2的n次冪.

 

 

         注意table的初始化是在第一次put的時候作的,那個時候還有考慮loadFactor再作一次tableSize的計算,那個時候獲得的就是最合適的那個2的n次冪的那個數了!厲害啊!

 

 

因此,看下put流程吧!這個很重要:

方法流程以下:

 

 

方法定義以下:

 

 

False表示,會改變existing value,至關因而key相同的話會作替換.

True表示,table不在creation mode.(這是啥意思?!)

 

第一次或者擴容的時候會調用resize():

  1. 若是是第一次初始化且沒有輸入initcapacity

那麼newCap=16,  threshold = 0.75 * 16

         Hash表大小爲: table = Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]

  1. 若是是第一次初始化,可是設置了initcapacity和loadFactor

注意,此時的threadhold(=initcapacity接近的2^n那個值) 和 loadFactor已經有值了,可是table==null,由於沒有初始化嘛,因此此時oldCap=0

  • oldThreadHold = threadhold(=initcapacity接近的2^n那個值)

此時 newCap = threadhold

 threshold =newThreadHold = newCap * loadFactor (threadhold還變小了!…)

Hash表大小爲: table = Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]

  1. 若是不是第一次初始化

此時 oldTab !=null  oldCap = oldTable.length  oldThreadHold = threadhold

那麼 newCap = oldCap << 1 爲原來的一倍 newThreadholder也爲oldThr的一倍,即都擴大爲原先的一倍!

         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

         還要將原先舊Table裏的數據轉移到新的table中.

  1. 插入完成後,還要檢查下是否須要擴容

即 ++size > threshold  那麼須要進行resize

 

         這裏讓我想到有問題的可能就是這個了,首次初始化,cap爲16,輸入的initcapacity爲15的時候,會涉及到一次擴容(若是沒有衝突的話—因此這裏的擴容一倍啥的操做應該是取了個折中!否則爲了避免衝突再擴容一倍很耗費空間啊!並且再擴容一倍也不能保證不衝突)

 

resize()流程分析:

(和Java7不一樣,8中resize後元素順序是不變的)

         歸納起來就是:

  1. 原先有值的狀況下

若是已是MAXIMUM_CAPACITY,那麼返回原先數組,不然將容量擴大爲原來的一倍,即newCap = oldCap << 1, newThr=oldThr << 1

  1. 原先沒有值,可是構造函數指定了initCapacity

newCap = oldThr, newThr隨後會被指定爲newCap*loadFactor

  1. 原先沒有值,且沒有指定initCapacity,即無參構造函數

按照默認的值初始化,即initCapacity=16, newThr=0.75*16

  1. 而後使用newCap構建一個newTab,若是舊錶不爲空就要遷移數據

遷移數據的流程以下:

 

 

  1. 若是隻有一個元素時,那就從新計算位置,插入新的table
  2. 若是節點是樹類型,那使用樹的插入方式(這個暫時還不太瞭解)
  3. 若是節點是鏈表類型,由於元素放的位置取決於tab[i = (capacity - 1) & hash]。當長度擴爲原來的2倍時,由於oldCap和newCap是2的次冪,而且newCap是oldCap的兩倍,就至關於oldCap的惟一一個二進制的1向高位移動了一位,結論爲元素要麼在原先位置,要麼在原位置上再移動2次冪。

舉例:

好比原來容量是16,那麼就至關於index=e.hash & 0x1111

如今容量擴大了一倍,就是32,那麼index=e.hash & 0x11111

如今(e.hash & oldCap) == 0就代表:

已知: e.hash & 0x1111 = index

而且: e.hash & 0x10000 = 0

那麼: e.hash & 0x11111 不也就是原先index的值!

 

get()流程分析:

         若是獲取到的Node爲null則返回null,不然返回Node中存儲的值。

 

 

         點到getNode()中繼續查看

 

 

         分四步進行:

                  首先,若是table未空,直接返回null。不然就查找節點

                  再者,計算hash獲得的位置剛符合,那直接返回。(只有一個bin的狀況)

                  若是是紅黑樹,按紅黑樹的方式查找

                  若是是鏈表,逐個查找。 找不到返回null。

 

 

entrySet分析:

         遍歷的話,使用此種方式。比每次從Map中從新獲取一個key要快多了!

不是每次都是new EntrySet(),可是暫時沒找到這個東西在哪裏填充的。

 

 

         網上的說法是遍歷的原理就是hashmap實現的原理。entrySet()該方法返回的是map包含的映射集合視圖,視圖的概念至關於數據庫中視圖。提供一個窗口,沒有具體到相關數據,而真正獲取數據仍是從table[]中來。(ps:能夠借鑑下hashmap的foreach方法,即先遍歷完數組中的第一個鏈表,再遍歷數組中的下一個鏈表…)

 

 

 

 

HashMap爲何線程不安全:

         Java8之前線程不安全是在於在resize()的時候會在get的時候產生死循環,而之因此產生死循環是由於resize以後,元素的前後順序會相反。

         即轉移的時候是這樣的:每次取出舊數組的頭結點的next,以後從新計算頭結點在新的Hash中的位置,而後將頭節點的next指向新的table[i],而後把table[i]設置成當前的頭結點,那麼就完成了頭結點的轉移。

   

 

         這時候,線程一種3.next是7,線程二中7.next是3,e.next = newTable[i]就會造成了環形鏈表,因此在get的時候就會一直循環在這裏。

而且在迭代的過程當中,若是有線程修改了map,會拋出ConcurrentModificationException錯誤,就是所謂的fail-fast策略。

 

         Java8以後,由於順序是相同的,因此上面的那個環形鏈表問題就沒有了。可是後面那個問題仍是有的,因此仍是線程不安全的。另外還有++size的操做也不是線程安全的!

 

 

 

 

 

 

 

參考:

http://www.iteye.com/topic/539465 (initailCapacity爲何要設置成2的n次冪?)

https://blog.csdn.net/dog250/article/details/46665743#comments (紅黑樹的一種解釋)

https://coolshell.cn/articles/9606.html(Java7中爲何會造成環形鏈表)

相關文章
相關標籤/搜索