JDK1.8 不一樣的HashMap

solution發表於2021-09-09
前言

HashMap想必大家都很熟悉JDK1.8 的 HashMap 隨便一搜都是一大片一大片的那為什麼還要寫呢我會把它精簡一下一方面有利於自己的學習另一方面希望讓大家更好理解核心內容。本篇主要講解HashMap原始碼的主要流程。本篇借鑑了  我們一起來看下JDK1.8做了哪些最佳化~

JDK1.7 VS JDK1.8 原始碼比較

最佳化概述之後會一一細說

  1. resize 擴容最佳化

  2. 引入了紅黑樹目的是避免單條連結串列過長而影響查詢效率紅黑樹演算法請參考 http://blog.csdn.net/v_july_v/article/details/6105630 HashMap整體結構如圖
    圖片描述

  3. 解決了多執行緒死迴圈問題但仍是非執行緒安全的多執行緒時可能會造成資料丟失問題。

JDK1.7 VS JDK1.8 效能比較
  • Hash較均勻的情況圖片描述

  • Hash不均勻的情況圖片描述

JDK1.8 中的 HashMap 是不是666的飛起效能碾壓JDK1.7中的HashMap~ 但是原始碼可比JDK1.7難讀一些了接下來一起來學習下 HashMap 的原始碼提前透露下核心方法是 resize 和 putVal

預備知識
  1. HashMap 中 table 角標計算及table.length 始終為2的冪即 2 ^ n對應的程式碼是

   /**
     * Returns a power of two size for the given target capacity.
     */
    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 = MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }    // 取key的hashCode值、高位運算、取模運算
    // 在JDK1.8的實現中最佳化了高位運算的演算法
    // 透過hashCode()的高16位異或低16位實現的(h = k.hashCode()) ^ (h >>> 16)
    // 主要是從速度、功效、質量來考慮的這麼做可以在陣列table的length比較小的時候
    // 也能保證考慮到高低Bit都參與到Hash的計算中同時不會有太大的開銷。
    static final int hash(Object key) {        int h;        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

我們在程式碼中經常會看到這樣計算table索引
圖片描述
這就是 table.length 為何是 2 ^ n 的原因了圖中 n 為 table的長度
圖片描述
這樣計算之後 在 n 為 2 ^ n 時 其實相當於 hash % n& 當然比 % 效率高這也是HashMap 計算角標時的巧妙之處。

  1. capacity、threshold和loadFactor之間的關係

  • capacity table的容量預設容量是16

  • threshold table擴容的臨界值

  • loadFactor 負載因子一般 threshold = capacity * loadFactor預設的負載因子0.75是對空間和時間效率的一個平衡選擇建議大家不要修改。

基本元素(原 Entity)
   static class Node implements Map.Entry {        final int hash;  // node的hash值
        final K key; // node的key
        V value; // node的value
        Node next; // node指向下一個node的引用

        Node(int hash, K key, V value, Node next) {            this.hash = hash;            this.key = key;            this.value = value;            this.next = next;
        }        public final K getKey()        { return key; }        public final V getValue()      { return value; }        public final String toString() { return key + "=" + value; }        public final int hashCode() {            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;            return oldValue;
        }        public final boolean equals(Object o) {            if (o == this)                return true;            if (o instanceof Map.Entry) {
                Map.Entry,?> e = (Map.Entry,?>)o;                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))                    return true;
            }            return false;
        }
    }
resize()方法

我們將resize()方法分為兩部分第一部分是生成newTable的過程第二部分是遷移資料。

final Node[] resize() {
        Node[] oldTab = table;        int oldCap = (oldTab == null) ? 0 : oldTab.length;        int oldThr = threshold;        int newCap, newThr = 0;        if (oldCap > 0) {            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;                return oldTab;
            }            else if ((newCap = oldCap = DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr  0) // initial capacity was placed in threshold
            newCap = oldThr;        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }        if (newThr == 0) {            float ft = (float)newCap * loadFactor;
            newThr = (newCap [] newTab = (Node[])new Node[newCap];
        table = newTab;
        ...        return newTab;
    }

以上程式碼展示了 newTable 的建立過程由於 table、capacity、threshold等是懶載入所以會有一系列的判斷及對應的初始化這些不是特別重要重點在下邊註釋標在程式碼塊上紅黑樹較為複雜這裡不做講解後續會考慮單講紅黑樹

if (oldTab != null) {    for (int j = 0; j  e;        if ((e = oldTab[j]) != null) { 
            oldTab[j] = null;            if (e.next == null)   // 如果只有table[j]中有元素
                newTab[e.hash & (newCap - 1)] = e;            else if (e instanceof TreeNode)  // 如果e是紅黑樹節點走紅黑樹替換方式
                ((TreeNode)e).split(this, newTab, j, oldCap);            else { // 如果 table[j] 後是一個連結串列 將原連結串列拆分為兩條鏈分別放到newTab中 
                Node loHead = null, loTail = null;
                Node hiHead = null, hiTail = null;
                Node next;                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;
                }
            }
        }
    }
}

重點就在這個 連結串列拆分首次看到 e.hash & oldCap 我是懵逼的。。。其實這樣是一個取巧的辦法效能上優於rehash的過程我們用圖解方式去解釋如何進行連結串列拆分
圖片描述(a) 是未擴容時 key1 和 key2 得出的 hash & (n - 1) 均為 5。
(b) 是擴容之後key1 計算出的 newTab 角標依舊為 5但是 key2 由於 擴容 得出的角標 加了 16即21 16是oldTab的length再來看e.hash & oldCapoldCap.length即n 本身為 0000 0000 0000 0000 0000 0000 0001 0000 這個位與運算可以得出擴容後哪些key 在 擴容新增位時1哪些是0一個位運算替換了rehash過程是不是得給100個贊~大概擴容的過程如下
圖片描述

執行緒安全問題

JDK1.7 HashMap在多執行緒的擴容時確實會出現迴圈引用導致下次get時死迴圈的問題具體可以參考。很多文章在說到死迴圈時都以JDK1.7來舉例其實JDK1.8的最佳化已經避免了死迴圈這個問題但是會造成資料丟失問題下面我舉個例子需要對應上邊resize的下半部分程式碼

建立 thread1 和 thread2 去新增資料此時都在resize兩個執行緒分別建立了兩個newTable並且thread1在table = newTab;處排程到thread2(沒有給table賦值)等待thread2擴容之後再排程回thread1注意擴容時oldTab[j] = null; 也就將 oldTable中都清掉了當回到thread1時將table指向thread1的newTable但訪問oldTable中的元素全部為null所以造成了資料丟失。

putVal()方法

put方法其實呼叫了putVal引數onlyIfAbsent表示如果為true若put的位置已經有value則不修改putIfAbsent方法中傳true這個方法的重點在於 TREEIFY_THRESHOLD 這個變數如果連結串列長度 >= TREEIFY_THRESHOLD - 1 則呼叫 treeifyBin  方法 從它的註釋上可以看出這個方法會把這條鏈所有的Node變為紅黑樹結構。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,               boolean evict) {
    Node[] tab; Node p; int n, i;    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);    else {
        Node e; K k;        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;        else if (p instanceof TreeNode)
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);        else {            for (int binCount = 0; ; ++binCount) {                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);                    break;
                }                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))                    break;
                p = e;
            }
        }        if (e != null) { // existing mapping for key
            V oldValue = e.value;            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);            return oldValue;
        }
    }
    ++modCount;    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);    return null;
}/**
 * Replaces all linked nodes in bin at index for given hash unless
 * table is too small, in which case resizes instead.
 */final void treeifyBin(Node[] tab, int hash) {      int n, index; Node e;        if (tab == null || (n = tab.length)  hd = null, tl = null;            do {
                TreeNode p = replacementTreeNode(e, null);  // Node 替換為 TreeNode
                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);   // 紅黑樹轉換過程
        }
}
entrySet()遍歷

我們在遍歷HashMap的時候都會使用 map.entrySet().iterator()看下這個 iterator 是什麼

public Set> entrySet() {
        Set> es;        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }    final class EntrySet extends AbstractSet> {
        ...        public final Iterator> iterator() {            return new EntryIterator();
        }
        ...
    }    final class EntryIterator extends HashIterator
        implements Iterator> {        public final Map.Entry next() { return nextNode(); }
    }    abstract class HashIterator {
        Node next;        // next entry to return
        Node current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node[] t = table;
            current = next = null;
            index = 0;            if (t != null && size > 0) { // advance to first entry
                do {} while (index  nextNode() {
            Node[] t;
            Node e = next;            if (modCount != expectedModCount)                throw new ConcurrentModificationException();            if (e == null)                throw new NoSuchElementException();            if ((next = (current = e).next) == null && (t = table) != null) {                do {} while (index  p = current;            if (p == null)                throw new IllegalStateException();            if (modCount != expectedModCount)                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

跟 ArrayListLinkedList一樣還是modCount expectedModCount 的問題expectedModCountiterator構造時賦的值等於當時的modCount所以如果已經生成了iterator如果擅自使用map.put()等操作會使modCount變化導致expectedModCount != modCount會丟擲ConcurrentModificationException 

結尾

好了以上除了紅黑樹HashMap中的我認為的核心內容至此就說完了可以看出JDK一直在許多細節上不斷地在做最佳化作為我們還是需要不斷地修煉去發現這些程式碼中的驚豔之處

原文連結http://www.apkbus.com/blog-487165-76822.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2459/viewspace-2812739/,如需轉載,請註明出處,否則將追究法律責任。

相關文章