HashMap、Hash Table、ConcurrentHashMap

wudidamowang666發表於2019-08-03

這個這個。。。本王最近由於開始找實習工作了,所以就在牛客網上刷一些公司的面試題,大多都是一些java,前端HTML,js,jquery,以及一些好久沒有碰的演算法題,說實話,有點難受,其實在我不知道的很多是地方還有很多很多的知識漏洞,就像這一次寫的這個,也是我在刷題的時候感覺到真的是我空缺的地方,為什麼呢?因為,做多了,錯多了。然而很尷尬的又是因為這個只是也是很多公司的面試題,所以索性直接寫下來整理一遍。

在這裡我也建議各位,牛客網不僅僅是一個找工作的station也是一個可以鍛鍊我們的地方,沒事刷刷題啊,逛逛論壇啊,說不定就能找到很多你意想不到的東西。

在面試的過程中,有幾個問題是比較常見的。

  1. HashTable、HashMap、ConcurrentHashMap的區別?
  2. HashMap執行緒不安全的出現場景?
  3. HashMap put方法存放資料時是怎麼判斷是否重複的?   
  4. JDK7和JDK8 中HashMap的實現有什麼區別?
  5. HashMap的長度為什麼是2的冪次方?

只要把這幾個問題過一遍之後,大致瞭解了他們各自的作用與互相之間的區別再!!去敲一遍其實就可以掌握了。

HashTable

  • 底層陣列+連結串列實現,無論key還是value都不能為null,執行緒安全,實現執行緒安全的方式是在修改資料時鎖住整個HashTable,效率低,ConcurrentHashMap做了相關優化
  • 初始size為11,擴容:newsize = oldsize*2+1
  • 計算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap

  • 在底層陣列+連結串列中實現,執行緒不安全,可儲存null鍵和null值
  • 初始size為16,可擴容:newsize = oldsize*2,size一定為2的n次冪
  • 擴容針對整個Map,每次擴容的時候,原來陣列中的元素依次重新計算存放的位置,並重新插入
  • 插入元素後才判斷該不該擴容,有可能無效擴容(插入後如果擴容,如果沒有再次插入,就會產生無效擴容)
  • 當Map中元素總數超過Entry陣列的75%,觸發擴容操作,為了減少連結串列長度,元素分配更均勻
  • 計算index方法:index = hash & (tab.length – 1)

*HashMap的初始值還要考慮載入因子:
雜湊衝突:若干Key的雜湊值按陣列大小取模後,如果落在同一個陣列下標上,將組成一條Entry鏈,對Key的查詢需要遍歷Entry鏈上的每個元素執行equals()比較。
載入因子:為了降低雜湊衝突的概率,預設當HashMap中的鍵值對達到陣列大小的75%時,即會觸發擴容。因此,如果預估容量是100,即需要設定100/0.75=134的陣列大小。
空間換時間:如果希望加快Key查詢的時間,還可以進一步降低載入因子,加大初始大小,以降低雜湊衝突的概率。

HashMap與HashTable的區別(面試題常考~)

1.兩者所繼承的父類不同

HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary類。不過它們都實現了同時實現了map、Cloneable(可複製)、Serializable(可序列化)這三個介面。

在這裡原本是擷取了JDK API1.6 中文版裡面的,但實在是太醜了,就在別人的部落格,呵呵,悄咪咪的拿了過來借鑑了一下

 2.兩者對外介面是不同的

HashTable比HashMap多提供了elements()和contains()兩個方法。

elements()方法繼承自HashTable的父類Doctionnary。elements()方法用於返回此時HashTable中的值的列舉。

contains()方法判斷該Hashtable是否包含傳入的value。它的作用與containsValue()一致。事實上,contansValue() 就只是呼叫了一下contains() 方法,是判斷雜湊表中是否包含指定的值。如圖是contains的原始碼:

public virtual bool Contains(object key)
{
    return this.ContainsKey(key);
}

3.對Null key 和Null value的支援不同
Hashtable既不支援Null key也不支援Null value。

HashMap中,key-value都是存在Entry中的。null可以作為鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值為null,不保證元素的順序恆久不變,它的底層使用的是陣列和連結串列,用過HashCode()方法和equal()方法來保證鍵的唯一性。當get()方法返回null值時,可能是 HashMap中沒有該鍵,也可能使該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。
4.執行緒安全的不同性

Hashtable是執行緒安全的,它的每個方法中都加入了Synchronize方法。在多執行緒併發的環境下,可以直接使用Hashtable,不需要自己為它的方法實現同步。

HashMap不是執行緒安全的,在多執行緒併發的環境下,可能會產生死鎖等問題。所以使用HashMap時就必須要自己增加同步處理,

雖然HashMap不是執行緒安全的,但是它的效率會比Hashtable要好很多。這樣設計是合理的。在我們的日常使用當中,大部分時間是單執行緒操作的。HashMap把這部分操作解放出來了。當需要多執行緒操作的時候可以使用執行緒安全的ConcurrentHashMap。ConcurrentHashMap雖然也是執行緒安全的,但是它的效率比Hashtable要高好多倍。因為ConcurrentHashMap使用了分段鎖,並不對整個資料進行鎖定。
5.Hash值的計算方法不同

為了求得元素的位置,需要根據元素的Key計算出一個雜湊值,然後再用這個雜湊值來計算出崔忠的位置。

Hashtable直接使用物件的hashCode。hashCode是JDK根據物件的地址或者字串或者數字算出來的int型別的數值。然後再使用除留餘數發來獲得最終的位置。

Hashtable在計算元素的位置時需要進行一次除法運算,而除法運算是比較耗時的。

HashMap為了提高計算效率,將雜湊表的大小固定為了2的冪,這樣在取模預算時,不需要做除法,只需要做位運算。位運算比除法的效率要高很多。

HashMap的效率雖然提高了,但是hash衝突卻也增加了。因為它得出的hash值的低位相同的概率比較高,而計算位運算

為了解決這個問題,HashMap重新根據hashcode計算hash值後,又對hash值做了一些運算來打散資料。使得取得的位置更加分散,從而減少了hash衝突。當然了,為了高效,HashMap只做了一些簡單的位處理。從而不至於把使用2 的冪次方帶來的效率提升給抵消掉。

ConcurrentHashMap

  • 底層採用分段的陣列+連結串列實現,執行緒安全。
  • key和value都不能為null。
  • 通過把整個Map分為N個Segment,可以提供相同的執行緒安全,但是效率提升N倍,預設提升16倍。
  • Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓執行緒獨佔,ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術
  • 有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖
  • 擴容:段內擴容(段內元素超過該段對應Entry陣列長度的75%觸發擴容,不會對整個Map進行擴容),插入前檢測需不需要擴容,有效避免無效擴容

這個就很棒了,上面總結的源於某猿大神,Java5提供的ConcurrentHashMap就像是HashTable的升級版,擴容性更強。

在HashMap中,通過get()返回的null值,既可以表示返回該Key所對應過的Value是null值,也可以表示為沒有該Key,在這種情況下就應該採用ConcurrentHashMap。

來看一張簡單的類圖:

ConcurrentHashMap是由Segment陣列結構和HashEntry陣列結構組成。Segment是一個可重入鎖(ReentrantLock),在ConcurrentHashMap裡扮演鎖的角色;HashEntry則用於儲存鍵值對資料。一個ConcurrentHashMap裡包含一個Segment陣列。Segment的結構和HashMap類似,是一種陣列和連結串列結構。一個Segment裡包含一個HashEntry陣列,每個HashEntry是一個連結串列結構的元素,每個Segment守護著一個HashEntry陣列裡的元素。當對HashEntry陣列的資料進行修改時,必須首先獲得與它對應的segment鎖。

 Hashtable中採用的鎖機制是一次鎖住整個hash表,從而在同一時刻只能由一個執行緒對其進行操作;而ConcurrentHashMap中則是一次鎖住一個桶。

簡單理解就是,ConcurrentHashMap是一個Segment陣列,Segment通過繼承ReentrantLock來進行加鎖,所以每次需要加鎖的操作鎖住的是一個Segment,只要保證每個Segment是執行緒安全的,也就實現了全域性的執行緒安全。重申一下,Segment陣列不能擴容,擴容是Segment陣列某個位置內部的陣列HashEntry<K,V>[]進行擴容,擴容後,容量為原來的2倍。可以回顧下出發擴容的地方,put的時候,如果判斷該值的插入會導致該Segment的元素個數超過閾值,那麼先進行擴容,再插值。

相關文章