1. HashMap從入門到熟悉
1. hash碰撞的解決方案
HashMap就是使用哈希表來存儲的。哈希表爲解決衝突,可以採用開放地址法和鏈地址法等來解決問題,Java中HashMap採用了鏈地址法。鏈地址法,簡單來說,就是數組加鏈表的結合。在每個數組元素上都有一個鏈表結構,當數據被Hash後,得到數組下標,把數據放在對應下標元素的鏈表上。
2. 紅黑樹優化方案
2.1 爲什麼是長度爲8的時候發生轉換
Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
理想情況下,在隨機哈希代碼下,桶中的節點頻率遵循泊松分佈,文中給出了桶長度k的頻率表。
由頻率表可以看出,桶的長度超過8的概率非常非常小。所以作者應該是根據概率統計而選擇了8作爲閥值,由此可見,這個選擇是非常嚴謹和科學的。
2.2 既然存在鏈表轉換爲紅黑樹,那麼是否存在紅黑樹轉換爲鏈表
HashMap在jdk1.8之後引入了紅黑樹的概念,表示若桶中鏈表元素超過8時,會自動轉化成紅黑樹;若桶中元素小於等於6時,樹結構還原成鏈表形式。
- 紅黑樹的平均查找長度是log(n),長度爲8,查找長度爲log(8)=3,鏈表的平均查找長度爲n/2,當長度爲8時,平均查找長度爲8/2=4,這纔有轉換成樹的必要;鏈表長度如果是小於等於6,6/2=3,雖然速度也很快的,但是轉化爲樹結構和生成樹的時間並不會太短。
- 還有選擇6和8的原因是:
- 中間有個差值7可以防止鏈表和樹之間頻繁的轉換。假設一下,如果設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,如果一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。
3. 擴容發生的時間,爲什麼擴容是2倍,擴容的過程
3.1 擴容發生的時間
大於等於閾值—即當前數組的長度乘以加載因子的值的時候,就要自動擴容。
- 負載因子
- 過小:容易發生reszie,消耗性能
- 過大:容易發生hash碰撞,鏈表變長,紅黑樹變高
3.2 爲什麼hashmap底層數組要保證是2的n次方
//hash值的計算分爲兩步:
//1. 異或運算
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 爲第一步 取hashCode值
// h ^ (h >>> 16) 爲第二步 高位參與運算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//2. 和數組長度與運算,分佈到原有數組中
hash = h&(n-1)
得到 hash 值之後,再與數組的長度-1(length-1)進行一次與運算,因爲如果數組的長度是 2 的倍數,那麼length-1 的二進制一定是 …00001111…這種形式,也就是前面一定都是 0,後面全是1,那麼再與 hash 值進行與運算的時候,結果一定是在原來數組大小的範圍內,比如默認數組大小16-1=15 的二進制爲: 00000000 00000000 00000000 00001111,某 key 的hash 值爲:11010010 00000001 10010000 00100100,那麼與上面做與運算的時候,值會對後面的四位進行運算,肯定會落在0~15 的範圍內,假如不是 2 的倍數,那麼 length-1 的二進制後面就不可能全是 1,做與運算的時候就會造成空間浪費。
3.3 擴容的具體過程
- 開闢了新的數組空間
- 元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。
我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖爲16擴充爲32的resize示意圖:
4 既然存在擴容,是否存在縮容
沒有縮容機制,沒有看到與resize()對應方法。
5 HashMap和HashTable、HashSet、LinkedHashMap
- Hashtable:Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類,並且是線程安全的,任一時間只有一個線程能寫Hashtable,併發性不如ConcurrentHashMap,因爲ConcurrentHashMap引入了分段鎖。Hashtable不建議在新代碼中使用,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以ConcurrentHashMap替換。
- LinkedHashMap:LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。
- TreeMap:TreeMap實現SortedMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator遍歷TreeMap時,得到的記錄是排過序的。如果使用排序的映射,建議使用TreeMap。在使用TreeMap時,key必須實現Comparable接口或者在構造TreeMap傳入自定義的Comparator,否則會在運行時拋出java.lang.ClassCastException類型的異常。
5.2 HashMap和HashSet的區別
6. Hashmap爲什麼是線程不安全的【死鎖分析】
- 表面原因
- Hashmap的方法沒有使用synchronized進行同步
- 實際原因
- 如果能找到併發環境下的問題,就能證明是不安全的
- 併發環境下,hashmap進入擴容的時候容易造成Entry鏈成環,在查詢等操作的時候容易造成死循環
7. TreeMap和HashMap有什麼區別
使用Iterator迭代器遍歷的時候,HashMap的結果是沒有排序的,而TreeMap輸出的結果是排好序的。
參考資料
Ref1:https://tech.meituan.com/2016/06/24/java-hashmap.html
https://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/