HashMap之Hash碰撞衝突解決方案及將來改進

說明:參考網上的兩篇文章作了簡單的總結,以備後查(http://blogread.cn/it/article/7191?f=wb  ,http://it.deepinmind.com/%E6%80%A7%E8%83%BD/2014/04/24/hashmap-performance-in-java-8.html) html

1.HashMap位置決定與存儲

   經過前面的源碼分析可知,HashMap 採用一種所謂的「Hash 算法」來決定每一個元素的存儲位置。當程序執行put(String,Obect)方法 時,系統將調用String的 hashCode() 方法獲得其 hashCode 值——每一個 Java 對象都有 hashCode() 方法,均可經過該方法得到它的 hashCode 值。獲得這個對象的 hashCode 值以後,系統會根據該 hashCode 值來決定該元素的存儲位置。源碼以下:java

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }   
 static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
	    V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

    咱們知道Entry含有的屬性是Value,Key,還有一隻指向下一個指針Next。當系統決定存儲 HashMap 中的 key-value 對時,徹底沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每一個 Entry 的存儲位置。這也說明了前面的結論:咱們徹底能夠把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置以後,value 隨之保存在那裏便可算法

   hashmap

2.Hash碰撞產生及解決

   Hashmap裏面的bucket出現了單鏈表的形式,散列表要解決的一個問題就是散列值的衝突問題,一般是兩種方法:鏈表法和開放地址法。鏈表法就是將相同hash值的對象組織成一個鏈表放在hash值對應的槽位;開放地址法是經過一個探測算法,當某個槽位已經被佔據的狀況下繼續查找下一個可使用的槽位。java.util.HashMap採用的鏈表法的方式,鏈表是單向鏈表。造成單鏈表的核心代碼以下:數組

    void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

  

   上面方法的代碼很簡單,但其中包含了一個設計:系統老是將新添加的 Entry 對象放入 table 數組的 bucketIndex 索引處——若是 bucketIndex 索引處已經有了一個 Entry 對象,那新添加的 Entry 對象指向原有的 Entry 對象(產生一個 Entry 鏈),若是 bucketIndex 索引處沒有 Entry 對象,也就是上面程序代碼的 e 變量是 null,也就是新放入的 Entry 對象指向 null,也就是沒有產生 Entry 鏈。 HashMap裏面沒有出現hash衝突時,沒有造成單鏈表時,hashmap查找元素很快,get()方法可以直接定位到元素,可是出現單鏈表後,單個bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每一個 Entry,直到找到想搜索的 Entry 爲止——若是剛好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最先放入該 bucket 中),那系統必須循環到最後才能找到該元素。服務器

   經過上面可知若是多個hashCode()的值落到同一個桶內的時候,這些值是存儲到一個鏈表中的。最壞的狀況下,全部的key都映射到同一個桶中,這樣hashmap就退化成了一個鏈表——查找時間從O(1)到O(n)。也就是說咱們是經過鏈表的方式來解決這個Hash碰撞問題的。app

3.Hash碰撞性能分析

  

  Java 7:隨着HashMap的大小的增加,get()方法的開銷也愈來愈大。因爲全部的記錄都在同一個桶裏的超長鏈表內,平均查詢一條記錄就須要遍歷一半的列表。不過Java 8的表現要好許多!它是一個log的曲線,所以它的性能要好上好幾個數量級。儘管有嚴重的哈希碰撞,已經是最壞的狀況了,但這個一樣的基準測試在JDK8中的時間複雜度是O(logn)。單獨來看JDK 8的曲線的話會更清楚,這是一個對數線性分佈:wordpress

4.Java8碰撞優化提高

   爲何會有這麼大的性能提高,儘管這裏用的是大O符號(大O描述的是漸近上界)?其實這個優化在JEP-180中已經提到了。若是某個桶中的記錄過大的話(當前是TREEIFY_THRESHOLD = 8),HashMap會動態的使用一個專門的treemap實現來替換掉它。這樣作的結果會更好,是O(logn),而不是糟糕的O(n)。它是如何工做的?前面產生衝突的那些KEY對應的記錄只是簡單的追加到一個鏈表後面,這些記錄只能經過遍從來進行查找。可是超過這個閾值後HashMap開始將列表升級成一個二叉樹,使用哈希值做爲樹的分支變量,若是兩個哈希值不等,但指向同一個桶的話,較大的那個會插入到右子樹裏。若是哈希值相等,HashMap但願key值最好是實現了Comparable接口的,這樣它能夠按照順序來進行插入。這對HashMap的key來講並非必須的,不過若是實現了固然最好。若是沒有實現這個接口,在出現嚴重的哈希碰撞的時候,你就並別期望能得到性能提高了。這個性能提高有什麼用處?比方說惡意的程序,若是它知道咱們用的是哈希算法,它可能會發送大量的請求,致使產生嚴重的哈希碰撞。而後不停的訪問這些key就能顯著的影響服務器的性能,這樣就造成了一次拒絕服務攻擊(DoS)。JDK 8中從O(n)到O(logn)的飛躍,能夠有效地防止相似的攻擊,同時也讓HashMap性能的可預測性稍微加強了一些。源碼分析

相關文章
相關標籤/搜索