HashMap 底層實現、加載因子、容量值及死循環

HashMap 簡介

HashMap是一個基於哈希表實現的無序的key-value容器,它鍵和值容許設置爲 null,同時它是線程不安全的。java

HashMap 底層實現

  • jdk 1.7HashMap是以數組+鏈表的實現的
  • jdk1.8開始引入紅黑樹,HashMap底層變成了數組+鏈表+紅黑樹實現

紅黑樹簡介

紅黑樹是一種特殊的平衡二叉樹,它有以下的特徵:數組

  • 節點是紅色或黑色
  • 根節點是黑色的
  • 全部葉子都是黑色。(葉子是NULL節點)
  • 每一個紅色節點的兩個子節點都是黑色的(從每一個葉子到根的全部路徑上不能有兩個連續的紅色節點)
  • 從任一節點到其每一個葉子的全部路徑都包含相同數目的黑色節點。

因此紅黑樹的時間複雜度爲: O(lgn)安全

9358d109b3de9c828cdb8e7c6481800a18d84382.jpeg

jdk1.8:數組+鏈表+紅黑樹

HashMap的底層首先是一個數組,元素存放的數組索引值就是由該元素的哈希值(key-valuekey的哈希值)肯定的,這就可能產生一種特殊狀況——不一樣的key哈希值相同。數據結構

在這樣的狀況下,因而引入鏈表,若是key的哈希值相同,在數組的該索引中存放一個鏈表,這個鏈表就包含了全部key的哈希值相同的value值,這就解決了哈希衝突的問題。多線程

可是若是發生大量哈希值相同的特殊狀況,致使鏈表很長,就會嚴重影響HashMap的性能,由於鏈表的查詢效率須要遍歷全部節點。因而在jdk1.8引入了紅黑樹,當鏈表的長度大於8,且HashMap的容量大於64的時候,就會將鏈表轉化爲紅黑樹。ide

// jdk1.8
// HashMap#putVal

// binCount 是該鏈表的長度計數器,當鏈表長度大於等於8時,執行樹化方法
// TREEIFY_THRESHOLD = 8
if (binCount >= TREEIFY_THRESHOLD - 1)
    treeifyBin(tab, hash);

// HashMap#treeifyBin    
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // MIN_TREEIFY_CAPACITY=64
    // 若 HashMap 的大小小於64,僅擴容,不樹化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

加載因子爲何是0.75

所謂的加載因子,也叫擴容因子或者負載因子,它是用來進行擴容判斷的。函數

假設加載因子是0.5,HashMap初始化容量是16,當HashMap中有16 * 0.5=8個元素時,HashMap就會進行擴容操做。性能

HashMap中加載因子爲0.75,是考慮到了性能和容量的平衡。this

由加載因子的定義,能夠知道它的取值範圍是(0, 1]。spa

  • 若是加載因子太小,那麼擴容門檻低,擴容頻繁,這雖然能使元素存儲得更稀疏,有效避免了哈希衝突發生,同時操做性能較高,可是會佔用更多的空間。
  • 若是加載因子過大,那麼擴容門檻高,擴容不頻繁,雖然佔用的空間下降了,可是這會致使元素存儲密集,發生哈希衝突的機率大大提升,從而致使存儲元素的數據結構更加複雜(用於解決哈希衝突),最終致使操做性能下降。
  • 還有一個因素是爲了提高擴容效率。由於HashMap的容量(size屬性,構造函數中的initialCapacity變量)有一個要求:它必定是2的冪。因此加載因子選擇了0.75就能夠保證它與容量的乘積爲整數。
// 構造函數
public HashMap(int initialCapacity, float loadFactor) {
    // ……
    this.loadFactor = loadFactor;// 加載因子
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * Returns a power of two size for the given target capacity.返回2的冪
 * MAXIMUM_CAPACITY = 1 << 30
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

HashMap 的容量爲何是2的 n 次冪

HashMap的默認初始容量是16,而每次擴容是擴容爲原來的2倍。這裏的16和2倍就保證了 HashMap的容量是2的n次冪,那麼這樣設計的緣由是什麼呢?

緣由一:與運算高效

與運算&,基於二進制數值,同時爲1結果爲1,不然就是0。如1&1=1,1&0=0,0&0=0。使用與運算的緣由就是對於計算機來講,與運算十分高效。

緣由二:有利於元素充分散列,減小 Hash 碰撞

在給HashMap添加元素的putVal函數中,有這樣一段代碼:

// n爲容量,hash爲該元素的hash值
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

它會在添加元素時,經過i = (n - 1) & hash計算該元素在HashMap中的位置。

當 HashMap 的容量爲 2 的 n 次冪時,他的二進制值是100000……(n個0),因此n-1的值就是011111……(n個1),這樣的話(n - 1) & hash的值纔可以充分散列。

舉個例子,假設容量爲16,如今有哈希值爲1111,1110,1011,1001四種將被添加,它們與n-1(15的二進制=01111)的哈希值分別爲11十一、11十、11十、1011,都不相同。

而假設容量不爲2的n次冪,假設爲10,那麼它與上述四個哈希值進行與運算的結果分別是:010一、0100、000一、0001。

能夠看到後兩個值發生了碰撞,從中能夠看出,非2的n次冪會加大哈希碰撞的機率。因此 HashMap 的容量設置爲2的n次冪有利於元素的充分散列。

參考:HashMap初始容量爲何是2的n次冪及擴容爲何是2倍的形式

HashMap 是如何致使死循環的

HashMap會致使死循環是在jdk1.7中,因爲擴容時的操做是使用頭插法,在多線程的環境下可能產生循環鏈表,由此致使了死循環。在jdk1.8中改成使用尾插法,避免了該死循環的狀況。

在網上找到了比較詳細的解釋分析博客與視頻:

老生常談,HashMap的死循環【基於JDK1.7】

jdk1.7及1.8HashMap,ConcurrentHashMap實現原理,本身使用,侵刪

相關文章
相關標籤/搜索