HashMap源碼解析(JDK8)

整體介紹

HashMap實現Map接口,用於存儲key-value結構,能夠根據其key快速查找其value。底層實現爲採用一個table數組的hash表,數組中的每一項爲一個鏈表結構。對於每個key,先計算其hash值,然後根據hash值計算其在table數組中的位置,若該位置沒有元素,則直接將其放置在該位置,否則,則出現hash衝突,需要遍歷查看其所在bucket是否已經有該key了(通過hash和key進行比較),若有了直接替換該key對應的value,否則在鏈表頭部插入。

需要注意的是如果一個桶中元素大於某個閾值,在JDK8中會將其右鏈表轉換爲紅黑樹。而且對於哈希表(table數組)太滿時(大於負載因子),需要對其進行再散列,負載因子默認爲0.75,如果表中超過了75%的位置已經填入了元素,那麼這個表就會用雙倍的桶數自動進行再散列。

源碼解析

1. 成員變量

主要有以下幾個成員變量

/*靜態常量*/
//初始容量:16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//負載因子:0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//鏈表轉換爲紅黑樹的閾值:大於8
static final int TREEIFY_THRESHOLD = 8;
//紅黑數轉換爲鏈表的閾值:小於6
static final int UNTREEIFY_THRESHOLD = 6;

/*成員變量*/
//哈希表數組
transient Node<K,V>[] table;
//存儲HashMap中的key-value對
transient Set<Map.Entry<K,V>> entrySet;
//元素實際個數
transient int size;
//是否重新散列的閾值
int threshold;
//負載因子
final float loadFactor;

2. 存儲結構

主要存儲結構爲Node<K,V>的結點,用於表示鏈表結構。

還有用於表示紅黑樹結構的TreeNode<K,V>

3. 構造函數

在這裏插入圖片描述
在這裏插入圖片描述
可以看到,構造函數裏並沒有對table數組初始化,JDK8的初始化是放在第一次添加的時候進行的。

4. put方法

這是HashMap裏的最核心的方法了。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

首先計算key的hash值,然後調用putValue方法。計算hash值的代碼如下

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash函數

這裏有個問題:爲什麼這樣不是直接計算hashCode呢,還要與高16位做異或運算?

這裏的hash函數相當於是一個打擾函數,最終是減少碰撞。因爲我們是要根據hash值來計算其在table數組中的位置,也就是後面的tab[i = (n - 1) & hash],這裏(n-1) & hash相當於取模運算,但比取模更高效,因爲table的長度始終是2的n次方(初始容量16,後面擴容時也始終<<1),所以其低位相當於全是1,高位全是0,最終&運算只保留hash的低位。所以在table容量較小時,如n-1爲15(1111),hash和其相與後真正參與運算的也就是低4位,高位都爲0了,這樣可能會增加碰撞。所以在計算hash函數中將其和右移16位的值(高位16位變成0,低16位爲之前的高16位)進行異或,由於右移始終爲0,所以異或後原來的高位保持不變(原來是1的還是1,0的爲0),低位變成低位與高位的異或,這樣增加了低位的隨機性,混合了高位和低位,高位的信息也被保留在低位中了。
在這裏插入圖片描述
參考:

  1. https://www.cnblogs.com/NathanYang/p/9427456.html
  2. https://www.cnblogs.com/qfxydtk/p/8734784.html

putVal方法

在這裏插入圖片描述
上述有幾處需要注意的地方:

  • 1 首先是判斷table是否爲空,即初次添加,是則調用resize()函數進行初始化
  • 2 當沒有發生碰撞時,即tab[(n-1) & hash]位置爲null,直接添加元素
  • 3 如果發生碰撞,則首先判斷該位置的hash以及key是否相等,如相等則記錄下來(後續直接替換)
  • 4 判斷該位置結點是否是紅黑樹結構,是的話就執行紅黑樹的插入方法
  • 5 遍歷找是否有和待插入元素相等的key,找到則替換,沒有的話則直接在尾部插入(JDK8以前是在頭部插入),插入後如果發現其大於轉換爲紅黑樹的閾值,則將其轉換爲紅黑樹結構

resize方法


在這裏插入圖片描述

5. get方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

其調用的是getNode方法,根據key的hash和key找到該值。
在這裏插入圖片描述

6. remove方法

也是和get方法相似,根據key的hash值找到對應位置,然後分情況刪除。

HashMap總結

  • 底層實現:數組+鏈表+紅黑樹,允許key爲null
  • 負載因子的默認值是0.75:初始值大了,可減少哈希表的再散列(擴容的次數),但同時會導致散列衝突的可能性變大。初始值小了,可以減小散列衝突的可能性,但同時擴容的次數可能就會變多。
  • 初始容量的默認值是16:初始容量過大,遍歷時速度就會受影響,初始容量過小,散列表再散列(擴容的次數)可能就變得多
  • HashMap在計算hash值並不是直接根據key的hashCode,而是將其和高16位進行異或,增加其隨機性。
  • 並不是桶子上有8位元素的時候它就能變成紅黑樹,它得同時滿足我們的散列表容量大於64才行.

線程安全的HashMap

由於HashMap是線程不安全的,即多個線程可以同時put、get等,這在多線程環境下會出現問題,所以java又提供了線程安全的HashMap。

1. Hashtable

與HashMap存儲結構基本相同,底層實現是數組+鏈表,其是線程安全的,實現方式是對整個Hashtable加鎖(基本在所有操縱Hashtable的方法上都加了sysynchronized進行同步,所以同一時間只允許一個線程操作),且不允許key和value爲null,但是其實現效率較低,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換。Hashtable默認初始容量爲11,擴容方式爲原始容量x2 + 1.

2. ConcurrentHashMap

與Hashtable一樣都是用來實現線程安全的HashMap,但是卻比Hashtable效率高很多。主要是因爲其採用了鎖分段的機制,將數據分段存儲,每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問,所以可以供多個線程同時訪問,而不是像Hashtable那樣鎖住整個表,同一時刻只能有一個線程訪問。
ConcurrentHashMap從JDK1.5開始隨java.util.concurrent包一起引入JDK中,主要爲了解決HashMap線程不安全和Hashtable效率不高的問題。

補充:JDK8和7實現差異

  • JDK7中使用的是分段鎖機制,即將整個table分成多個Segment,對每個Segment加鎖,即有一個Segment數組,每一個Segment都是一個單獨的哈希表。在執行put操作時首先根據hash算法定位到元素屬於哪個Segment,然後對該Segment加鎖即可
  • JDK8對其進行了優化,底層採用和Node數組+鏈表+紅黑樹(和HashMap類似),併發控制使用Synchronized和CAS來操作,其與HashMap更接近。

volatile

volatile是java提供的一個關鍵字,用volatile修飾的成員變量可保證在多線程環境中的可見性。什麼是可見性呢?這和java的內存模型有關了(JMM),java虛擬機有自己的內存模型(對底層硬件內存模型的抽象),分爲主內存和本地私有內存,主內存是所有線程共享的,共享變量存放在這裏,同時每個線程都有自己的本地內存(工作內存),保存了線程使用到的主內存變量中的副本。

假設有被volatile修飾的成員變量value,線程A修改了它,實際修改的是線程A工作內存裏的value(主內存中的副本),還沒有同步到主內存中,因此線程B讀取的value還是原來的value,這樣就不同步了。而採用volatile修飾的變量當某個線程修改了這個值時,會立即刷新到主內存中,同時通知其他線程對該變量的操作需要從主內存去重新讀取該變量的值(而不是已經在工作內存中的緩存值)。

ConcurrentHashMap中就是用了volatile來修飾一些成員變量:

  • 針對table數組的可見性
    在這裏插入圖片描述
  • 針對table中數組元素的可見性(Node中的val和next)
    在這裏插入圖片描述
    主內存和工作內存示意圖:
    在這裏插入圖片描述
    硬件內存模型:
    在這裏插入圖片描述

CAS

ConcurrentHashMap使用了CAS和Synchronized來進行同步。這兩者有什麼區別呢,CAS是一種樂觀鎖機制,即假設最好的情況,每次去訪問數據的時候都認爲沒有修改,所以不會上鎖,但是在更新的時候會去判斷在此期間有沒有別的線程更新這個數據,所以適用於讀多寫少的情況。Synchronized採用是一種悲觀鎖機制,即每次訪問資源都會上鎖,其他線程想訪問這個資源就好阻塞,適用於寫多讀少的情況。

CAS即Compare And Swap(比較與交換),涉及到三個操作數:

  • 需要讀寫的內存值V
  • 進行比較的值A(期待值)
  • 擬寫入的值B
    只有當內存值和期待值相等時,纔會用新的值B來更新內存中的值,否則不會執行任何操作,是一種原子操作(volatile不能提供原子操作)。一般情況下是一個自旋操作,即不斷的重試。

主要通過Unsafe類中的compareAndSwapIntcompareAndSwapLong等方法來實現:

public final native boolean compareAndSwapInt(java.lang.Object arg0, long arg1, int arg2, int arg3);

是一個本地方法實現的,arg0表示對象(obj),arg1表示地址(offset),arg2表示期望值(expect),arg3表示更新值(update),如果obj內的value和expect相等,就證明沒有其他線程改變過這個變量,那麼就更新它爲update。

參考:

HashMap原理:

  1. https://www.cnblogs.com/chenssy/p/3521565.html
  2. https://mp.weixin.qq.com/s/ubwe-2U19Y7GQsIByYTWng
  3. https://www.cnblogs.com/NathanYang/p/9427456.html

ConcurrentHashMap:

  1. https://yq.aliyun.com/articles/673765
  2. https://www.cnblogs.com/chengxiao/p/6528109.html

CAS原理:

  1. https://juejin.im/post/5a73cbbff265da4e807783f5
  2. https://juejin.im/post/5b4977ae5188251b146b2fc8
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章