1. 程式人生 > >HashMap原始碼探究(死鎖/擴容)【JDK1.7】【JDK1.8】

HashMap原始碼探究(死鎖/擴容)【JDK1.7】【JDK1.8】

先說HashMap最重要的一點:缺點

       HashMap的缺點我們大都聽說過,其在高併發的情況下表現較差,會出現一些奇奇怪怪的問題,比如使CPU使用率提高到100%此處打個小差,因為前幾天,我的伺服器莫名其妙CPU佔用率也達到了100%,我還以為是跑了哪個專案寫的有問題了,後來查了一下所有程序才發現有個ddgs的一直在高佔用,經過研究發現,這是一個新型的挖礦病毒,中毒原因是我之前練習redis的時候忘了設定密碼o(╥﹏╥)o),那麼它為什麼會出現這個原因呢?其實這個高併發下的問題,也和HashMap一個長久以來的缺點相掛鉤,沒錯,就是HashMap 的擴容機制。

        為什麼說HashMap的擴容機制是長久以來的缺點,我們可以簡單看一下其原始碼,可知:其初始化長度為16,擴容因子為0.75(即當內容達到百分之七十五的時候會擴容為當前的二倍),那麼問題其實就在於HashMap是怎樣進行擴容的。

          它在擴容的時候,會先生成一個新的HashMap,然後把原HashMap裡的資料一個一個的複製到新的HashMap裡,那麼就很輕易的知道了,當我們的資料量過大的時候,我們的HashMap會進行多次擴容,那麼就會相對來說很消耗我們的資源,解決這個缺點的辦法也很簡單,先大致預估一下我們需要在這個HashMap裡存放多少資料,然後在初始化它的時候給它先把預設長度給設定了,這樣就避免了多次擴容多次複製。

          介紹完了擴容機制,那麼其在高併發下那個100%的問題是怎麼來的呢,不難想出,上述步驟中,最有可能出現問題的,就是複製的那一步。

//擴容的方法
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
//在此進行我們所說的複製的那一步  傳入的引數為新HashMap, 初始化hash掩碼(此處不太懂也沒事)
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }


//最重要的方法
    void transfer(Entry[] newTable, boolean rehash) {
        //先儲存新陣列長度
        int newCapacity = newTable.length;
        //然後依次遍歷我們的老陣列,由此我們也可以知道,我們的複製是從老表的頭部開始的
        for (Entry<K,V> e : table) {
            //這個while迴圈是我們問題的關鍵
            while(null != e) {
    /*當我們遍歷到一個不為空的老資料的時候,我們假定這個老資料在橫向(我們知
道HashMap是由陣列和連結串列構成的,那麼假設一個二維空間裡HashMap縱向是陣列結構,橫向是連結串列結構)掛載這有下一個節點,
那麼我們現在想移動這個老資料,必須得保證我們下邊節點的資料不丟失,所以我們建立一個next先去"指向"它*/
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
        //計算我們的這個老資料在新陣列中的位置
                int i = indexFor(e.hash, newCapacity);
        //這一步是把我們的老資料在新陣列所在位置的元素(如果沒有也一樣)掛載到老資料後面
                e.next = newTable[i];
        //然後讓我們新陣列的這個位置指向我們已經掛載新資料後的老資料。
                newTable[i] = e;
        //此處是假設我們老數組裡,老資料下邊掛載的還有節點的情況
                e = next;
            }
        }
    }

在這個transfer方法中,當我們看到

 Entry<K,V> next = e.next;

的時候,我們就應該有這樣一個擔憂,在高併發的情況下,這一步會不會影響我們的擴容,答案是肯定的……

但是說這是不是一個BUG ?  並不 

sun公司的負責人表達的意思是:我們設計HashMap本來的作用就不應該是應對高併發情況的,在高併發的情況下,我們有另外一個更好用的ConcurrentHashMap 去應對。

HashMap的put方法注意點

我們通常會簡單認為HashMap的put方法的Key只進行一次hash運算,但事實上,HashMap的put實現是在計算key的hash之上,又進行了一次自己規定的位運算,以JDK1.8中原始碼為例(版本有差距,不過也都是進行了二次位運算)

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



     static final int hash(Object key) {
        int h;                                        //^異或運算  >>>為帶符號右移
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }