java hashMap知識(shí)點(diǎn)

寫在前

hashMap 在日常項(xiàng)目中用的筆記頻繁,大家都知道hashMap是線程不安全的,在并發(fā)情況下,應(yīng)該用concurrentHashMap,但是,為什么hashMap是線程不安全的,而concurrentHashMap是線程安全的呢?下面我們來具體分析下。

hashMap

大家都知道hashMap的底層是數(shù)組和鏈表的數(shù)據(jù)結(jié)構(gòu),下面是java 1.8中hashMap的數(shù)據(jù)結(jié)構(gòu)示意圖(圖片來源于網(wǎng)絡(luò)):


java 1.7 hashMap
java 1.8 hashMap

java 8 在數(shù)據(jù)結(jié)構(gòu)上對(duì)比1.7多了一個(gè)紅黑樹,當(dāng)鏈表的長(zhǎng)度大于8的時(shí)候會(huì)將鏈表轉(zhuǎn)化為紅黑樹,紅黑樹的是一個(gè)自平衡二叉樹,查找算法O(logn)。
在并發(fā)情況下,當(dāng)我們往hashMap中存放的數(shù)據(jù)過多的時(shí)候,尤其在hashMap擴(kuò)容的時(shí)候,在并發(fā)情況下,很容易出問題。

java1.7擴(kuò)容

在hashMap中put元素時(shí),如果capacity(容量)* loadFactor(裝載因子)大于hashMap中size(鍵值對(duì)的個(gè)數(shù))時(shí)就會(huì)發(fā)生擴(kuò)容。

 /**
     * 源碼分析:addEntry(hash, key, value, i)
     * 作用:添加鍵值對(duì)(Entry )到 HashMap中
     */
      void addEntry(int hash, K key, V value, int bucketIndex) {  
          // 參數(shù)3 = 插入數(shù)組table的索引位置 = 數(shù)組下標(biāo)

          // 1. 插入前,先判斷容量是否足夠
          // 1.1 若不足夠,則進(jìn)行擴(kuò)容(2倍)、重新計(jì)算Hash值、重新計(jì)算存儲(chǔ)數(shù)組下標(biāo)
          if ((size >= threshold) && (null != table[bucketIndex])) {  
            resize(2 * table.length); // a. 擴(kuò)容2倍  --> 分析1
            hash = (null != key) ? hash(key) : 0;  // b. 重新計(jì)算該Key對(duì)應(yīng)的hash值
            bucketIndex = indexFor(hash, table.length);  // c. 重新計(jì)算該Key對(duì)應(yīng)的hash值的存儲(chǔ)數(shù)組下標(biāo)位置
    }  
    // 1.2 若容量足夠,則創(chuàng)建1個(gè)新的數(shù)組元素(Entry) 并放入到數(shù)組中--> 分析2
    createEntry(hash, key, value, bucketIndex);  
} 
--------------------- 
/**
   * 分析1:resize(2 * table.length)
   * 作用:當(dāng)容量不足時(shí)(容量 > 閾值),則擴(kuò)容(擴(kuò)到2倍)
   */ 
   void resize(int newCapacity) {  

    // 1. 保存舊數(shù)組(old table) 
    Entry[] oldTable = table;  

    // 2. 保存舊容量(old capacity ),即數(shù)組長(zhǎng)度
    int oldCapacity = oldTable.length; 

    // 3. 若舊容量已經(jīng)是系統(tǒng)默認(rèn)最大容量了,那么將閾值設(shè)置成整型的最大值,退出    
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;  
        return;  
    }  

    // 4. 根據(jù)新容量(2倍容量)新建1個(gè)數(shù)組,即新table  
    Entry[] newTable = new Entry[newCapacity];  

    // 5. 將舊數(shù)組上的數(shù)據(jù)(鍵值對(duì))轉(zhuǎn)移到新table中,從而完成擴(kuò)容 ->>分析1.1 
    transfer(newTable); 

    // 6. 新數(shù)組table引用到HashMap的table屬性上
    table = newTable;  

    // 7. 重新設(shè)置閾值  
    threshold = (int)(newCapacity * loadFactor); 
} 
 /**
   * 分析1.1:transfer(newTable); 
   * 作用:將舊數(shù)組上的數(shù)據(jù)(鍵值對(duì))轉(zhuǎn)移到新table中,從而完成擴(kuò)容
   * 過程:按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入
   */ 
void transfer(Entry[] newTable) {
      // 1. src引用了舊數(shù)組
      Entry[] src = table; 

      // 2. 獲取新數(shù)組的大小 = 獲取新容量大小                 
      int newCapacity = newTable.length;

      // 3. 通過遍歷 舊數(shù)組,將舊數(shù)組上的數(shù)據(jù)(鍵值對(duì))轉(zhuǎn)移到新數(shù)組中
      for (int j = 0; j < src.length; j++) { 
          // 3.1 取得舊數(shù)組的每個(gè)元素  
          Entry<K,V> e = src[j];           
          if (e != null) {
              // 3.2 釋放舊數(shù)組的對(duì)象引用(for循環(huán)后,舊數(shù)組不再引用任何對(duì)象)
              src[j] = null; 

              do { 
                  // 3.3 遍歷 以該數(shù)組元素為首 的鏈表
                  // 注:轉(zhuǎn)移鏈表時(shí),因是單鏈表,故要保存下1個(gè)結(jié)點(diǎn),否則轉(zhuǎn)移后鏈表會(huì)斷開
                  Entry<K,V> next = e.next; 
                 // 3.4 重新計(jì)算每個(gè)元素的存儲(chǔ)位置
                 int i = indexFor(e.hash, newCapacity); 
                 // 3.5 將元素放在數(shù)組上:采用單鏈表的頭插入方式 = 在鏈表頭上存放數(shù)據(jù) = 將數(shù)組位置的原有數(shù)據(jù)放在后1個(gè)指針、將需放入的數(shù)據(jù)放到數(shù)組位置中
                 // 即 擴(kuò)容后,可能出現(xiàn)逆序:按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入
                 e.next = newTable[i]; 
                 newTable[i] = e;  
                 // 3.6 訪問下1個(gè)Entry鏈上的元素,如此不斷循環(huán),直到遍歷完該鏈表上的所有節(jié)點(diǎn)
                 e = next;             
             } while (e != null);
             // 如此不斷循環(huán),直到遍歷完數(shù)組上的所有數(shù)據(jù)元素
         }
     }
 }

在擴(kuò)容resize()過程中,在將舊數(shù)組上的數(shù)據(jù) 轉(zhuǎn)移到 新數(shù)組上時(shí),轉(zhuǎn)移操作是:按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入,即在轉(zhuǎn)移數(shù)據(jù)、擴(kuò)容后,出現(xiàn)鏈表逆序的情況(下面的過程參考了文章:https://www.cnblogs.com/dongguacai/p/5599100.html)。
正常情況下hashMap擴(kuò)容:
1、假設(shè)我們的hash算法是簡(jiǎn)單的key mod一下表的大?。磾?shù)組的長(zhǎng)度)。
2、最上面是old hash表,其中HASH表的size=2,所以key=3,5,7在mod 2 以后都沖突在table[1]這個(gè)位置上了。
3、接下來HASH表擴(kuò)容,resize=4,然后所有的<key,value>重新進(jìn)行散列分布,過程如下:

單線程擴(kuò)容.png

單線程情況下,沒有任何問題。
并發(fā)情況下hashMap擴(kuò)容:
假設(shè)我們有兩個(gè)線程,分別用紅色和藍(lán)色標(biāo)注了。

void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;//①
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

假設(shè)線程1在執(zhí)行完①處阻塞,線程2,開始執(zhí)行,線程2執(zhí)行上面的代碼,這個(gè)時(shí)候的狀態(tài)是:

image.png

這里要特別清楚一點(diǎn),線程1和線程2的Entry<K,V> e 指向的是同一個(gè)數(shù)組對(duì)象,有一個(gè)改變了,另一個(gè)指向的內(nèi)容也就變了,另外,newTable數(shù)組是線程私有的。
接下來,線程1被喚醒,繼續(xù)執(zhí)行,此時(shí),e指向的是key=3的節(jié)點(diǎn),指向完后線程1的newTable[]中的數(shù)據(jù)為:
image.png

接著,e指向key=7的節(jié)點(diǎn),e!=null繼續(xù)執(zhí)行
next=e.next=3(線程2執(zhí)行完之后結(jié)構(gòu)發(fā)生了變化節(jié)點(diǎn)7指向了節(jié)點(diǎn)3)
e.next = newTable[i];節(jié)點(diǎn)7指向節(jié)點(diǎn)3(這時(shí)要看線程1的newTable)
newTable[i] = e;newTable[i]指向節(jié)點(diǎn)7
e = next;e節(jié)點(diǎn)指向下一個(gè)節(jié)點(diǎn)3


image.png

Entry<K,V> next = e.next e節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)為null;
e.next = newTable[i];3節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)指向節(jié)點(diǎn)7
newTable[i] = e;newTable[i]指向節(jié)點(diǎn)3;
e=next;e為空 結(jié)束循環(huán);


image.png

這次擴(kuò)容結(jié)束了。但是后續(xù)如果有查詢(無論是查詢的迭代還是擴(kuò)容),都會(huì)hang死在table【3】這個(gè)位置上。同時(shí),這個(gè)過程中發(fā)現(xiàn)節(jié)點(diǎn)5在線程1丟掉了,所以多線程下put,也可能造成元素丟失。

Java1.8擴(kuò)容

首先,看下hashMap中怎么計(jì)算key的hash值:

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

圖中的 hash 是由鍵的 hashCode 產(chǎn)生。計(jì)算余數(shù)時(shí),由于 n 比較小,hash 只有低4位參與了計(jì)算,高位的計(jì)算可以認(rèn)為是無效的。這樣導(dǎo)致了計(jì)算結(jié)果只與低位信息有關(guān),高位數(shù)據(jù)沒發(fā)揮作用。為了處理這個(gè)缺陷,我們可以上圖中的 hash 高4位數(shù)據(jù)與低4位數(shù)據(jù)進(jìn)行異或運(yùn)算,即 hash ^ (hash >>> 4)。通過這種方式,讓高位數(shù)據(jù)與低位數(shù)據(jù)進(jìn)行異或,以此加大低位信息的隨機(jī)性,變相的讓高位數(shù)據(jù)參與到計(jì)算中。此時(shí)的計(jì)算過程如下:


image.png

在 Java 中,hashCode 方法產(chǎn)生的 hash 是 int 類型,32 位寬。前16位為高位,后16位為低位,所以要右移16位。
下面重點(diǎn)講下hashMap的插入操作:

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

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 初始化桶數(shù)組 table,table 被延遲到插入新數(shù)據(jù)時(shí)再進(jìn)行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果桶中不包含鍵值對(duì)節(jié)點(diǎn)引用,則將新鍵值對(duì)節(jié)點(diǎn)的引用存入桶中即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 如果鍵的值以及節(jié)點(diǎn) hash 等于鏈表中的第一個(gè)鍵值對(duì)節(jié)點(diǎn)時(shí),則將 e 指向該鍵值對(duì)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            
        // 如果桶中的引用類型為 TreeNode,則調(diào)用紅黑樹的插入方法
        else if (p instanceof TreeNode)  
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 對(duì)鏈表進(jìn)行遍歷,并統(tǒng)計(jì)鏈表長(zhǎng)度
            for (int binCount = 0; ; ++binCount) {
                // 鏈表中不包含要插入的鍵值對(duì)節(jié)點(diǎn)時(shí),則將該節(jié)點(diǎn)接在鏈表的最后
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果鏈表長(zhǎng)度大于或等于樹化閾值,則進(jìn)行樹化操作
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                
                // 條件為 true,表示當(dāng)前鏈表包含要插入的鍵值對(duì),終止遍歷
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 判斷要插入的鍵值對(duì)是否存在 HashMap 中
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent 表示是否僅在 oldValue 為 null 的情況下更新鍵值對(duì)的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 鍵值對(duì)數(shù)量超過閾值時(shí),則進(jìn)行擴(kuò)容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

插入邏輯并不復(fù)雜,下面看下擴(kuò)容機(jī)制(一下內(nèi)容參考https://segmentfault.com/a/1190000012926722):
在 HashMap 中,桶數(shù)組的長(zhǎng)度均是2的冪,閾值大小為桶數(shù)組長(zhǎng)度與負(fù)載因子的乘積。當(dāng) HashMap 中的鍵值對(duì)數(shù)量超過閾值時(shí),進(jìn)行擴(kuò)容。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果 table 不為空,表明已經(jīng)初始化過了
    if (oldCap > 0) {
        // 當(dāng) table 容量超過容量最大值,則不再擴(kuò)容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        } 
        // 按舊容量和閾值的2倍計(jì)算新容量和閾值的大小
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    } else if (oldThr > 0) // initial capacity was placed in threshold
        /*
         * 初始化時(shí),將 threshold 的值賦值給 newCap,
         * HashMap 使用 threshold 變量暫時(shí)保存 initialCapacity 參數(shù)的值
         */ 
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        /*
         * 調(diào)用無參構(gòu)造方法時(shí),桶數(shù)組容量為默認(rèn)容量,
         * 閾值為默認(rèn)容量與默認(rèn)負(fù)載因子乘積
         */
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // newThr 為 0 時(shí),按閾值計(jì)算公式進(jìn)行計(jì)算
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    // 創(chuàng)建新的桶數(shù)組,桶數(shù)組的初始化也是在這里完成的
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 如果舊的桶數(shù)組不為空,則遍歷桶數(shù)組,并將鍵值對(duì)映射到新的桶數(shù)組中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 重新映射時(shí),需要對(duì)紅黑樹進(jìn)行拆分
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍歷鏈表,并將鏈表節(jié)點(diǎn)按原順序進(jìn)行分組
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 將分組后的鏈表映射到新桶中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

往底層數(shù)據(jù)結(jié)構(gòu)中插入節(jié)點(diǎn)時(shí),一般都是先通過模運(yùn)算計(jì)算桶位置,接著把節(jié)點(diǎn)放入桶中即可,在 JDK 1.8 中,則對(duì)這個(gè)過程進(jìn)行了一定的優(yōu)化


image.png

上圖中,桶數(shù)組大小 n = 16,hash1 與 hash2 不相等。但因?yàn)橹挥泻?位參與求余,所以結(jié)果相等。當(dāng)桶數(shù)組擴(kuò)容后,n 由16變成了32,對(duì)上面的 hash 值重新進(jìn)行映射:


image.png

擴(kuò)容后,參與模運(yùn)算的位數(shù)由4位變?yōu)榱?位。由于兩個(gè) hash 第5位的值是不一樣,所以兩個(gè) hash 算出的結(jié)果也不一樣。上面的計(jì)算過程并不難理解,繼續(xù)往下分析。
image.png

假設(shè)我們上圖的桶數(shù)組進(jìn)行擴(kuò)容,擴(kuò)容后容量 n = 16,重新映射過程如下:

依次遍歷鏈表,并計(jì)算節(jié)點(diǎn) hash & oldCap 的值。如下圖所示


image.png

如果值為0,將 loHead 和 loTail 指向這個(gè)節(jié)點(diǎn)。如果后面還有節(jié)點(diǎn) hash & oldCap 為0的話,則將節(jié)點(diǎn)鏈入 loHead 指向的鏈表中,并將 loTail 指向該節(jié)點(diǎn)。如果值為非0的話,則讓 hiHead 和 hiTail 指向該節(jié)點(diǎn)。完成遍歷后,可能會(huì)得到兩條鏈表,此時(shí)就完成了鏈表分組:


image.png

最后再將這兩條鏈接存放到相應(yīng)的桶中,完成擴(kuò)容。如下圖:
image.png

從上圖可以發(fā)現(xiàn),重新映射后,兩條鏈表中的節(jié)點(diǎn)順序并未發(fā)生變化,還是保持了擴(kuò)容前的順序。以上就是 JDK 1.8 中 HashMap 擴(kuò)容的代碼講解。另外再補(bǔ)充一下,JDK 1.8 版本下 HashMap 擴(kuò)容效率要高于之前版本。如果大家看過 JDK 1.7 的源碼會(huì)發(fā)現(xiàn),JDK 1.7 為了防止因 hash 碰撞引發(fā)的拒絕服務(wù)攻擊,在計(jì)算 hash 過程中引入隨機(jī)種子。以增強(qiáng) hash 的隨機(jī)性,使得鍵值對(duì)均勻分布在桶數(shù)組中。在擴(kuò)容過程中,相關(guān)方法會(huì)根據(jù)容量判斷是否需要生成新的隨機(jī)種子,并重新計(jì)算所有節(jié)點(diǎn)的 hash。而在 JDK 1.8 中,則通過引入紅黑樹替代了該種方式。從而避免了多次計(jì)算 hash 的操作,提高了擴(kuò)容效率。
雖然jdk1.8中hashMap擴(kuò)容避免了死循環(huán),但是,在并發(fā)情況下還是有可能取到空值的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容