這個這個。。。本王最近由於開始找實習工作了,所以就在牛客網上刷一些公司的面試題,大多都是一些java,前端HTML,js,jquery,以及一些好久沒有碰的演算法題,說實話,有點難受,其實在我不知道的很多是地方還有很多很多的知識漏洞,就像這一次寫的這個,也是我在刷題的時候感覺到真的是我空缺的地方,為什麼呢?因為,做多了,錯多了。然而很尷尬的又是因為這個只是也是很多公司的面試題,所以索性直接寫下來整理一遍。
在這裡我也建議各位,牛客網不僅僅是一個找工作的station也是一個可以鍛鍊我們的地方,沒事刷刷題啊,逛逛論壇啊,說不定就能找到很多你意想不到的東西。
在面試的過程中,有幾個問題是比較常見的。
- HashTable、HashMap、ConcurrentHashMap的區別?
- HashMap執行緒不安全的出現場景?
- HashMap put方法存放資料時是怎麼判斷是否重複的?
- JDK7和JDK8 中HashMap的實現有什麼區別?
- 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的元素個數超過閾值,那麼先進行擴容,再插值。