HashMap的實現原理

1,數據結構

      JDK1.8後添加紅黑樹,底層是數組+鏈表紅黑樹實現。

      鏈表長度>8 & 數組長度>=64 ====>  紅黑樹

      紅黑樹節點數<6  =====>鏈表

2,HashMap的插入原理

1,判斷數組爲空--->初始化,初始化,默認初始化容量(capactiy)16,必須是2的冪,最大值爲int最大值,默認初始化加載因子(factor)是0.75f,同時設置臨界值爲16*0.75f, 如果自己傳入初始大小k,初始化大小爲 大於k的 2的整數次方,例如如果傳10,大小爲16

2,數組不爲空 ==> 計算key的hash值,若key爲null,則放在table[0]位置,不爲空,則先計算hash,然後通過hash與table.length取模計算index的值,然後將key放到table[index]位置,

3,當table[index]已存在其它元素時,說明發生了hash衝突(存在二個節點key的hash值一樣),

4,繼續判斷key是否相等,相等,用新的value替換原數據

5,若key不相等,判斷當前節點類型是不是樹型節點,如果是樹型節點,創造樹型節點插入紅黑樹中;

6,如果不是樹型節點,創建普通Node加入鏈表中;判斷鏈表長度是否大於 8, 大於的話鏈表轉換爲紅黑樹;

7,若小於8,會在table[index]位置形成一個鏈表,將新添加的元素放在table[index],原來的元素通過Entry的next進行鏈接,這樣以鏈表形式解決hash衝突問題,

8,插入完成之後判斷當前節點數是否大於閾值,如果大於開始擴容爲原數組的二倍。

    這個閾值是8,紅黑樹轉鏈表的閾值是6;因爲只有長度N>=7的時候,紅黑樹的平均查找長度lgN纔會小於鏈表的平均查找長度N/2,這個可以畫函數圖來確定,lgN與N/2的交點處N約爲6.64。設置成8是爲了防止出現頻繁的鏈表與紅黑樹的轉換,當大於8的時候鏈表轉紅黑樹,小於6的時候紅黑樹轉鏈表,中間這一段作爲緩衝

3,HashMap的哈希函數怎麼設計?

hash函數是拿到key的hashCode值,是一個32位的int值,然後將hashcode的高16位和低16位進行異或操作

這樣設計的原因:

      1,儘可能降低hash碰撞,因此採用了位運算

      2,算法一定要高效,因爲這是高頻操作,因此採用位運算

注意:異或運算:如果a、b兩個值不相同,則異或結果爲1。如果a、b兩個值相同,異或結果爲0

4,爲什麼採用hashcode的高16位和低16位異或就能降低hash碰撞,hash函數能不能直接用key的hashcode?

       因爲key.hashcode()方法調用的是key鍵值類型自帶的hash函數,返回int類型散列值。int值的範圍是前後加起來大約是40億的映射空間。只要是hash函數映射的比較均勻鬆散,一般應用是很難出現碰撞的。但是40億長度的數組,內存肯定放不下。如果HashMap數組的初始大小才16,用之前還需要先對數組的長度進行模運算,得到的餘數才能用來訪問數組下標。所以用位運算,位運算比模運算快。這裏也剛好解釋了爲什麼HashMap的數組長度要取2的整數冪。因爲這樣,(數組長度-1)正好相當於一個“低位掩碼”。“與”操作的結果就是散列值的高位全部歸零,只保留低位值,用來做數組下標訪問。

5,爲什麼它使用紅黑樹不使用二叉樹?
     紅黑樹犧牲了一些查找性能,但是本身並不是完全平衡的二叉樹。因此插入刪除效率高,二叉樹是相反的。

6,你之前提到了負載因子,你知道它有什麼作用嗎?
      負載因子表示HashMap的擁擠程度,影響到hash操作到同一個數組位置的概率。默認的負載因子是0.75,當hashmap裏容納的元素達到數組長度的75%時,就會擴容,在HashMap的構造器中可以定製負載因子。

7,你現在可以給我講講get方法的過程嗎?
      計算出key的hashcode,根據hashcode定位該元素所在桶的下標,若桶爲空,則返回null,不爲空,遍歷Entry對象鏈表,直到找到元素,沒找到就返回null。

8,HashMap如何解決衝突?

  •  鏈地址法:將衝突的元素存入數組後面的鏈表中,hashMap中使用的方法就是鏈地址法,也就是數組+單鏈表
  •  再哈希:同時構造多個不同的hash函數,第一個衝突就使用第二個,以此類推
  •  建立公共溢出區:將衝突的元素存入數組後面的鏈表中,hashMap中使用的方法就是鏈地址法,也就是數組+單鏈表。
  •  開放地址法:從發生衝突的單元起,按照一定的順序從哈希表中找出一個空白單元,然後把衝突元素存入該單元的方法

9,JDK1.8對HashMap還做了哪些優化?

  • 數組+鏈表改成了數組+鏈表或紅黑樹;
  • 鏈表的插入方式從頭插法改成了尾插法,簡單說就是插入時,如果數組位置上已經有元素,1.7將新元素放到數組中,原始節點作爲新節點的後繼節點,1.8遍歷鏈表,將元素放置到鏈表的最後;
  • 擴容的時候1.7需要對原數組中的元素進行重新hash定位在新數組的位置,1.8採用更簡單的判斷邏輯,位置不變或索引+舊容量大小;
  • 在插入時,1.7先判斷是否需要擴容,再插入,1.8先進行插入,插入完成再判斷是否需要擴容;

10,爲什麼要做這種優化?

  1. 1.8使用紅黑樹:防止發生hash衝突,鏈表長度過長,將時間複雜度由O(n)降爲O(logn);
  2. 1.8使用尾插法:因爲1.7頭插法擴容時,頭插法會使鏈表發生反轉,多線程環境下會產生環;A線程在插入節點B,B線程也在插入,遇到容量不夠開始擴容,重新hash,放置元素,採用頭插法,後遍歷到的B節點放入了頭部,這樣形成了環
  3. 這是由於擴容是擴大爲原數組大小的2倍,用於計算數組位置的掩碼僅僅只是高位多了一個1,怎麼理解呢?擴容前長度爲16,用於計算(n-1) & hash 的二進制n-1爲0000 1111,擴容爲32後的二進制就高位多了1,爲0001 1111。因爲是& 運算,1和任何數 & 都是它本身,那就分二種情況,如下圖:原數據hashcode高位第4位爲0和高位爲1的情況;第四位高位爲0,重新hash數值不變,第四位爲1,重新hash數值比原來大16(舊數組的容量)

 

 11,那HashMap是線程安全的嗎?

       不是,在多線程環境下,1.7 會產生死循環、數據丟失、數據覆蓋的問題,1.8 中會有數據覆蓋的問題,以1.8爲例,當A線程判斷index位置爲空後正好掛起,B線程開始往index位置的寫入節點數據,這時A線程恢復現場,執行賦值操作,就把A線程的數據給覆蓋了;還有++size這個地方也會造成多線程同時擴容等問題。

怎麼解決線程不安全?

  • Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以實現線程安全的Map。
  • HashTable是直接在操作方法上加synchronized關鍵字,鎖住整個數組,鎖粒度比較大,
  • Collections.synchronizedMap是使用Collections集合工具的內部類,通過傳入Map封裝出一個SynchronizedMap對象,內部定義了一個對象鎖,方法通過對象鎖實現線程安全;
  • ConcurrentHashMap使用分段鎖,降低了鎖粒度,讓併發度大大提高。

12,ConcurrentHashMap的分段鎖的實現原理?

       ConcurrentHashMap成員變量使用volatile 修飾,免除了指令重排序,同時保證內存可見性,另外使用CAS操作和synchronized結合實現賦值操作,多線程操作只會鎖住當前操作索引的節點。(線程A鎖住A節點所在鏈表,線程B鎖住B節點所在鏈表,操作互不干涉)

13,鏈表轉紅黑樹是鏈表長度達到閾值,這個閾值是多少?爲什麼是8,不是16,32甚至是7 ?又爲什麼紅黑樹轉鏈表的閾值是  6,不是8了呢?

閾值是8,紅黑樹轉鏈表閾值爲6,

因爲經過計算,在hash函數設計合理的情況下,發生hash碰撞8次的機率爲百萬分之6,概率說話。。因爲8夠用了,至於爲什麼轉回來是6,因爲如果hash碰撞次數在8附近徘徊,會一直髮生鏈表和紅黑樹的互相轉化,爲了預防這種情況的發生,設置爲6

14,HashMap內部節點是有序的嗎?

        是無序的,根據hash值隨機插入

那有沒有有序的Map?

         LinkedHashMap 和 TreeMap

跟我講講LinkedHashMap怎麼實現有序的?

         LinkedHashMap內部維護了一個單鏈表,有頭尾節點,同時LinkedHashMap節點Entry內部除了繼承HashMap的Node屬性,還有before 和 after用於標識前置節點和後置節點。可以實現按插入的順序或訪問順序排序。

TreeMap怎麼實現有序的?

          TreeMap是按照Key的自然順序或者實現的Comprator接口的比較函數的順序進行排序,內部是通過紅黑樹來實現。所以要麼key所屬的類實現Comparable接口,或者自定義一個實現了Comparator接口的比較器,傳給TreeMap用於key的比較

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