HashMap源碼解析

對於HashMap,如果是java程序員,那麼定然不會陌生,對於HashMap,應該說是最常用的一種Map結構了,同樣在面試當中也會屢屢被提問到,常見的幾種題目:

  • HashMap的默認容量?
  • HashMap是如何擴容的?
  • HashMap的數組大小爲什麼一定是2的冪?
  • HashMap爲什麼是線程不安全的?
  • Java7到Java8做了哪些改進?爲什麼?

因爲重要,所以我也就學習源碼,並且將學習心得記錄下來,與大家一起學習。

首先 再看HashMap之前,我們來簡單回顧一下哈希表

哈希表是由一些基於哈希值的桶和鏈表所構成的。哈希桶就是可以快速檢索的數據結構,舉個例子 如果要尋找電話本的人的聯繫方式,我們可以利用拼音的首字母快速定位到這個聯繫人,存放這些字母的就叫哈希桶。本質上,哈希桶就是 將一個元素映射成一個可以快速檢索的哈希值。

哈希桶加上數組就構成了哈希表,數組的好處是隨機尋址的速度與長度無關,時間複雜度是O(1),但是哈希表最大的缺點是會發生碰撞,如果多個元素的哈希值是相同的,那麼我們就說哈希值發生了碰撞,爲了解決這個問題,我們將數組換成了鏈表,找到哈希值時,通過哈希桶裏可以精確的找到所要查找的元素。平均差找時間都是O(1)。在java世界中,我們用來表達哈希表的數據結構就是HashMap。

image.png

哈希桶的實現是由hashcode實現的int hashcode 底下有對象:object1,object2....

 

我們先來看java7的HashMap。

經典的數據結構:數組加鏈表。

通過查看源碼 我們可以知曉HashMap的默認容量爲你16,如果改變負載因子 並且將初始值設爲17 結果會怎麼樣麼?再次點開源碼,我們會發現他會向上取整爲2的冪也就是變成了32.之後我們可以看到負載因子是0.75.

這裏有一個問題,就是桶的初始化容量是16個,而我們的HashCode值是-2^31~2^31-1,42億個數。那麼我們怎麼從任意一個int變成0~n-1。聰明的你肯定想到了取餘這個想法,但是在這個方法有兩個缺點:

  • 負數取模還是負數,所以我們需要把負數變爲正數
  • 速率較慢

那麼我們點開源碼發現,爲了尋找要插入的元素的索引值,做了一個鬼畜的操作:hash&(length-1),做這樣一個位運算,那麼這操作正好可以解釋爲什麼我們需要輸進去的HashMap的初始容量是2的冪,我們可以看出hash&(2^n-1),最後的下標就是Hashmap對應的2^n-1的個數下標,之所以把容量定爲2的冪,就是因爲讓2^n-1位運算時拿到的值全部是1,這樣做與運算就可以快速找到下標並且分佈還是均勻的(妙啊)。

解釋完這個問題 ,我們來看看是怎麼擴容的,根據源碼,我麼可以看到,當所需要的容量超過原始容量*負載因子(0.75)時,就需要進行擴容,擴容的大小是之前的兩倍。而在擴容的過程中就包含了一個rehash的操作。那麼在擴容的時候,會進行transfer也就是將之前的數據進行遷移的操作,遍歷所有的元素,並且重新計算哈希值,找到在新表裏的索引,把它放進去。這是一個巨坑!!!爲了避免高位不同,低位相同,進行了高位與低位的異或操作。

在Java7中的HashMap會有很多問題:

  1. 會碰到死鎖
  2. CVE-2011-1858  TOMCAT郵件組的討論

對於死鎖問題,其實多數是程序員自己的問題,因爲HashMap本身就不是線程安全的,當在多個線程中使用hashmap的時候,就必須給他加入同步的環境.

對於TOMCAT郵件組的討論,是說一個安全問題,如果我們的多個元素映射成同一個哈希值時,會把哈希表退化成一個鏈表,而鏈表的查詢的時間複雜度的o(n),那麼這個查詢速度是很可怕的,如果是被黑客利用精心構造的成千上萬個元素,具有同樣的哈希值,就可以引起Dos攻擊,針對這個問題,sun公司提供了一個小補丁,就是如果檢查到元素是String的話那麼就該用另外一種不同於默認的hash算法來去避免潛在的危險。

此時,針對於這兩個問題,我們迎來了Java8之後的HashMap

Java8對於之前的HashMap之前做了哪些改進呢?

  • 由之前數組/鏈表--->數組/紅黑樹
  • 擴容時插入順序的改進
  • 函數方法
    • foreach
    • compute系列
  • Map的新API
    • merge
    • replace

在Java8的源碼中,爲什麼變成樹的閾值是8?我們可以看到對於紅黑樹的容量服從參數爲0.5的泊松分佈,大於8的桶中的容量是10萬分之一,不易發生碰撞。

在進行put操作時,如果超過閾值,就把桶變爲紅黑樹,如果沒超過,還是用鏈表來實現。

在進行擴容操作時,掌握了java7時的操作,我們可以想到,擴容爲二倍,要麼索引值和之前一樣,要麼就是最高位是在之前索引值的基礎上再加一個一,就相當於是把原先的變成了兩個鏈表,一個是高位的鏈表,一個是低位的鏈表,在把它賦值給新的桶中去。從而緩解了死鎖問題(生成環的問題)。

 

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