面試中的HashMap、Hashtable與ConcurrentHashMap

面試中的HashMap、Hashtable與ConcurrentHashMap

近兩年出場率最高的 Java 面試知識點之一

問題1:說說你瞭解的 HashMap、HashTable 和 ConcurrentHashMap

作死:平時用用,沒看的那麼深
簡答:這三者都是 Map 的實現,HashMap 是基於哈希表的 Map 接口的實現, Hashtable 同樣是基於哈希表,但是 Hashtable 是線程安全的,ConcurrentHashMap 是從1.5 版本出現的,線程安全的可以替代 Hashtable 的 Map 實現。
詳答:(以上重複的不再多說) HashMap 採用哈希表來管理元素,在調用 put 方法向 HashMap 中寫入值的時候,先計算 key 的哈希值來進行快速定位,然後寫入值或者替換值。
當然,也會有多個不同元素計算出同一個哈希值,這就是我們說的哈希碰撞,此時再配合 equals 方法,共同判斷這個 Key 是否真正的存在,然後再進行下面的操作。

關於鍵值存儲:HashMap 允許存在 null 的鍵和值,但是鍵值只能有一個爲 null,Hashtable 不允許有 null 的鍵值存在。
Hashtable 的默認初始化大小是 11 ,影響因子同樣是 0.75,擴容採用的是在影響因子的約束下 的 rehash() 方法 newCapacity = (oldCapacity << 1) + 1

public Hashtable() {
        this(11, 0.75f);
}

Hashtable 可以算是1.4 遺留類,現在還有很多冗餘和可優化項。而且單線程下也要加鎖,所以現在的代碼中,多線程通常採用 ConcurrentHashMap 來實現。

ConcurrentHashMap 見問題4~

問題2:聽說 HashMap 在 1.7 和 1.8 版本有不同,說說看

作死:有區別麼,不是一樣的麼?
簡答:是底層數據結構的實現發生了變化
詳述:在 1.7 版本中,HashMap 底層採用了 Entry 數組+鏈表的數據結構,通過對 Key 的 Hashcode 進行取模來決定 key 存放的位置。相同 Hashcode (或者說取模後) 的 key 值會存儲到一個 Entry 中,他們會形成一個鏈表 Node<>,一旦鏈表過長,查找複雜度可能會達到O(n)。

在 1.8 版本之後,HashMap 底層採用數組 + 鏈表/紅黑樹的數據結構,在同 Hashcode 的 key 的數量小於等於8個的時候,還是採用鏈表的結構,如果大於8個,那麼就採用紅黑樹的數據結構。

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);

因爲紅黑樹的性能特點,在大量 hashcode 值相同的時候,查找某個特定元素,也只是需要O(log n) 的開銷.

但是想要使用好1.8版本的 HashMap,請正確的實現 Compare 接口,如果實現的不好或者沒實現,效率可能還要低於 1.7 版本。

問題3:來說說 HashMap 的擴容機制

作死: 沒太研究過,可能是翻倍吧
簡答: 初始化容量16,超過的話是原來的二倍
詳細:默認情況下,HashMap 初始化的容量大小爲 16,同時還有約束 HashMap 什麼時候擴容的影響因子爲 0.75。

// 默認初始化大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 默認影響因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

影響因子的存在,是爲了維持 HashMap 中的空間開銷與時間開銷。
當 HashMap 的容量超過 16*0.75=12 的時候,HashMap 調用resize() 中 newCap = oldCap << 1 << 1 的方法開始第一次擴容。
如果說一個HashMap 中有12個鍵值對,那麼它的大小爲16,如果是13的時候,那麼他它大小即爲32。

我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。

HashMap 中用到了大量的位運算,這是我們在設計自己的工具類中可以借鑑的。

在這,我從美團點評技術團隊博客盜了個圖,非常有益於我們理解 HashMap 的 put 和 擴容機制,感謝美團點評技術團隊(無利益,侵刪)~
這裏寫圖片描述

問題4:詳細說說你知道的 ConcurrentHashMap

對於ConcurrentHashMap,同樣的,從 1.7 到 1.8 版本,Java開發者對 ConcurrentHashMap 也做了修改。

在 1.7 版本中,大小默認爲16,ConcurrentHashMap 由一個 Segment 數組和多個 HashEntry 組成。使用 Segment 是鎖分離技術的實現,將一個大的 table 轉換爲多個小的 table 來進行加鎖,每個 Segment 元素存儲的是一個Entry數組+鏈表,與 HashMap 結構相同。

到了 1.8 版本,初始化大小爲 16 ,0.75。ConcurrentHashMap 拋棄了 Segment 數組,採用 Node數組 + 鏈表 + 紅黑樹 的數據結構實現,當鏈表長度大於8的時候會構建爲紅黑樹。併發控制使用Synchronized和CAS來操作(整個看起來就像是優化過且線程安全的HashMap)。

ConcurrentHashMap 的 put 操作是併發進行的,所以可能會發生 put 與擴容操作同時進行的情況,那麼先擴容,再 put 。只有發生衝突的時候,才採用樂觀鎖的方式進行併發處理。

關於 ConcurrentHashMap 涉及到併發和樂觀鎖的擴容,嗯….給你個鏈接
http://www.importnew.com/22007.html

總的來說,就是 put擴容 的同時,要考慮到鎖的問題,不考慮鎖的情況下,大體上和 HashMap 是相似的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章