HashMap、HashTable及ConcurrentHashMap區別及工作原理
前言
第一次寫部落格,水平有限可能有理解不到位或理解錯的地方。歡迎各位大神參與討論或指正。
Map在工作中的使用頻率較高,HashMap相關的問題在面試中也經常被問到。所以抽空在網上找資料對它們進行了系統的學習,作出以下幾點總結:
- HashMap、HashTable及ConcurrentHashMap的區別。
- HashMap的工作原理。
- 相關面試題。
一、HashMap、HashTable及ConcurrentHashMap的區別
1. 底層實現/容納資料
- hashMap:底層陣列+連結串列實現;key和value都可為null。初始容量為16。擴容:newsize = oldsize*2,size一定為2的n次冪。
- hashTable:底層陣列+連結串列實現;key和value都不能為null。初始容量為11。擴容:newsize = olesize*2+1。
- ConcurrentHashMap:底層採用分段的陣列+連結串列實現。初始容量為16。擴容:段內擴容,不會對整個Map擴容。
2. 執行緒安全與效率
- hashMap:執行緒不安全,效率相對較高。多執行緒下擴容resize可能出現死迴圈現象。
- hashTable:執行緒安全。實現執行緒安全的方式是在修改資料時鎖住整個HashTable,效率低。(原因:synchronized是針對整張Hash表的,即每次鎖住整張表讓執行緒獨佔)
- ConcurrentHashMap: 執行緒安全。通過把整個Map分為N個Segment,可以提供相同的執行緒安全,同時使效率提升N倍,預設提升16倍。(讀操作不加鎖,由於HashEntry的value變數是 volatile的,也能保證讀取到最新的值。)
3. 迭代器(Iterator)
- hashMap:HashMap的迭代器(Iterator)是fail-fast迭代器。當其它執行緒改變了HashMap的結構(增加或者移除元素),將會丟擲ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會丟擲ConcurrentModificationException異常。
- hashTable:Hashtable的enumerator迭代器不是fail-fast的。
- ConcurrentHashMap: 弱一致迭代器。
二、HashMap的工作原理
1. 底層思想
HashMap是基於hashing的原理,我們使用put(key, value)儲存物件到HashMap中,使用get(key)從HashMap中獲取物件。從而實現對資料的讀寫。
2. 工作原理
- 鍵值對傳遞給put()方法時,呼叫鍵物件的hashCode()方法來計算hashcode;
- 通過hashcode找到bucket位置來儲存Entry(值)物件;
- 獲取物件時,通過鍵物件的equals()方法找到正確的鍵值對,然後返回值物件。
- HashMap使用連結串列來解決碰撞問題,當發生碰撞時,物件將會儲存在連結串列的下一個節點中。
- HashMap在每個連結串列節點中儲存鍵值對物件。當兩個不同的鍵物件的hashcode相同時,它們會儲存在同一個bucket位置的連結串列中,可通過鍵物件的equals()方法來找到鍵值對。如果連結串列大小超過閾值(TREEIFY_THRESHOLD,8),連結串列就會被改造為樹形結構。(原因:連結串列太長效率低)
三、相關面試題
1.當兩個物件的hashcode相同會發生什麼?
因為hashcode相同,所以它們的bucket位置相同,會發生‘碰撞’。因為HashMap使用連結串列儲存物件,這個Entry(包含有鍵值對的Map.Entry物件)會儲存在連結串列中。
2.如果兩個鍵的hashcode相同,你如何獲取值物件?
當我們呼叫get()方法,HashMap會使用鍵物件的hashcode找到bucket位置,然後獲取值物件。如果兩個值物件hashcode相同儲存在同一個bucket,會呼叫keys.equals()方法去找到連結串列中正確的節點,最終找到要找的值物件。
3.HashMap的初始值該如何計算?
HashMap的初始值根據負載因子計算。為了降低雜湊衝突的概率,預設當HashMap中的鍵值對達到陣列大小的75%時(負載因子預設0.75),即會觸發擴容。因此,如果預估容量是100,即需要設定100/0.75=134的陣列大小。所以此時應給256最為合適。(容量給2的N次方,原因在這兒就不詳細說了,大家可以網上搜索擴充套件一下這個知識)
4.HashTable和ConcurrentHashMap都是執行緒安全的,為什麼ConcurrentHashMap效率要高?
Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓執行緒獨佔,ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術。
注意
:有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。
5.如何讓HashMap同步?
Map m = Collections.synchronizeMap(hashMap);
6.如何減少/降低Hash碰撞?
使用不可變的、宣告作final的物件,並且採用合適的equals()和hashCode()方法的話,將會減少碰撞的發生,提高效率。不可變性使得能夠快取不同鍵的hashcode,這將提高整個獲取物件的速度,使用String,Interger這樣的wrapper類作為鍵是非常好的選擇。
7.如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
預設的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會建立原來HashMap大小的兩倍的bucket陣列,來重新調整map的大小,並將原來的物件放入新的bucket陣列中。這個過程叫作rehashing,因為它呼叫hash方法找到新的bucket位置。
8.重新調整HashMap大小會存在什麼問題?
多執行緒的情況下,可能產生條件競爭(race condition)。
當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個執行緒都發現HashMap需要重新調整大小了,它們會同時試著調整大小。在調整大小的過程中,儲存在連結串列中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在連結串列的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那麼就死迴圈了。這個時候,你可以質問面試官,為什麼這麼奇怪,要在多執行緒的環境下使用HashMap呢?
9.為什麼String, Interger這樣的wrapper類適合作為鍵?
String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那麼就不能從HashMap中找到你想要的物件。不可變性還有其他的優點如執行緒安全。如果你可以僅僅通過將某個field宣告成final就能保證hashCode是不變的,那麼請這麼做吧。因為獲取物件的時候要用到equals()和hashCode()方法,那麼鍵物件正確的重寫這兩個方法是非常重要的。如果兩個不相等的物件返回不同的hashcode的話,那麼碰撞的機率就會小些,這樣就能提高HashMap的效能。
**10.我們可以使用自定義的物件作為鍵嗎? **
這是前一個問題的延伸。當然你可能使用任何物件作為鍵,只要它遵守了equals()和hashCode()方法的定義規則,並且當物件插入到Map中之後將不會再改變了。如果這個自定義物件時不可變的,那麼它已經滿足了作為鍵的條件,因為當它建立之後就已經不能改變了。
四、結構圖
五、尊重原創
感謝閱讀。