HashMap 源碼分析(JDK1.8)

前言

HashMap想必大家都很熟悉烤礁,JDK1.8 的 HashMap 隨便一搜都是一大片一大片的关翎,那為什么還要寫(xiě)呢陆赋,我會(huì)把它精簡(jiǎn)一下沐祷,一方面有利于自己的學(xué)習(xí),另一方面希望讓大家更好理解核心內(nèi)容奏甫。本篇主要講解HashMap源碼的主要流程戈轿。本篇借鑒了 美團(tuán)的HashMap源碼解析 ,我們一起來(lái)看下JDK1.8做了哪些優(yōu)化~

JDK1.7 VS JDK1.8 源碼比較

優(yōu)化概述阵子,之后會(huì)一一細(xì)說(shuō):

  1. resize 擴(kuò)容優(yōu)化
  2. 引入了紅黑樹(shù)思杯,目的是避免單條鏈表過(guò)長(zhǎng)而影響查詢效率,紅黑樹(shù)算法請(qǐng)參考 http://blog.csdn.net/v_july_v/article/details/6105630, HashMap整體結(jié)構(gòu)如圖:
    image.png
  3. 解決了多線程死循環(huán)問(wèn)題色乾,但仍是非線程安全的誊册,多線程時(shí)可能會(huì)造成數(shù)據(jù)丟失問(wèn)題。
JDK1.7 VS JDK1.8 性能比較:
  • Hash較均勻的情況:
    image.png
  • Hash不均勻的情況:
    image.png

JDK1.8 中的 HashMap 是不是666的飛起暖璧,性能碾壓JDK1.7中的HashMap~ 但是源碼可比JDK1.7難讀一些了案怯,接下來(lái)一起來(lái)學(xué)習(xí)下 HashMap 的源碼,提前透露下澎办,核心方法是 resizeputVal嘲碱。

預(yù)備知識(shí)
  1. HashMap 中 table 角標(biāo)計(jì)算及table.length 始終為2的冪,即 2 ^ n局蚀,對(duì)應(yīng)的代碼是 :
   /**
     * 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 < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

    // 取key的hashCode值麦锯、高位運(yùn)算、取模運(yùn)算
    // 在JDK1.8的實(shí)現(xiàn)中琅绅,優(yōu)化了高位運(yùn)算的算法扶欣,
    // 通過(guò)hashCode()的高16位異或低16位實(shí)現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16),
    // 主要是從速度千扶、功效料祠、質(zhì)量來(lái)考慮的,這么做可以在數(shù)組table的length比較小的時(shí)候澎羞,
    // 也能保證考慮到高低Bit都參與到Hash的計(jì)算中髓绽,同時(shí)不會(huì)有太大的開(kāi)銷。
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

我們?cè)诖a中經(jīng)常會(huì)看到這樣計(jì)算table索引:

image.png

這就是 table.length 為何是 2 ^ n 的原因了煤痕,圖中 n 為 table的長(zhǎng)度:
image.png

這樣計(jì)算之后梧宫, 在 n 為 2 ^ n 時(shí)接谨, 其實(shí)相當(dāng)于 hash % n摆碉,& 當(dāng)然比 % 效率高,這也是HashMap 計(jì)算角標(biāo)時(shí)的巧妙之處脓豪。

  1. capacity巷帝、threshold和loadFactor之間的關(guān)系:
  • capacity table的容量,默認(rèn)容量是16
  • threshold table擴(kuò)容的臨界值
  • loadFactor 負(fù)載因子扫夜,一般 threshold = capacity * loadFactor楞泼,默認(rèn)的負(fù)載因子0.75是對(duì)空間和時(shí)間效率的一個(gè)平衡選擇,建議大家不要修改笤闯。
基本元素(原 Entity)
   static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;  // node的hash值
        final K key; // node的key
        V value; // node的value
        Node<K,V> next; // node指向下一個(gè)node的引用

        Node(int hash, K key, V value, Node<K,V> 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的過(guò)程,第二部分是遷移數(shù)據(jù)颗味。

final Node<K,V>[] resize() {
        Node<K,V>[] 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 << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (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 < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        ...
        return newTab;
    }

以上代碼展示了 newTable 的創(chuàng)建過(guò)程超陆,由于 table、capacity浦马、threshold等是懶加載时呀,所以會(huì)有一系列的判斷及對(duì)應(yīng)的初始化张漂,這些不是特別重要,重點(diǎn)在下邊谨娜,注釋標(biāo)在代碼塊上(紅黑樹(shù)較為復(fù)雜航攒,這里不做講解,后續(xù)會(huì)考慮單講紅黑樹(shù)):

if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {  // 遍歷 oldTab
        Node<K,V> 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是紅黑樹(shù)節(jié)點(diǎn)趴梢,走紅黑樹(shù)替換方式
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
            else { // 如果 table[j] 后是一個(gè)鏈表 漠畜,將原鏈表拆分為兩條鏈,分別放到newTab中 
                Node<K,V> loHead = null, loTail = null;
                Node<K,V> hiHead = null, hiTail = null;
                Node<K,V> 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;
                }
            }
        }
    }
}

重點(diǎn)就在這個(gè) 鏈表拆分坞靶,首次看到 e.hash & oldCap 我是懵逼的盆驹。。滩愁。其實(shí)這樣是一個(gè)取巧的辦法躯喇,性能上優(yōu)于rehash的過(guò)程,我們用圖解方式去解釋如何進(jìn)行鏈表拆分:

image.png
(a) 是未擴(kuò)容時(shí)硝枉, key1key2 得出的 hash & (n - 1) 均為 5廉丽。
(b) 是擴(kuò)容之后,key1 計(jì)算出的 newTab 角標(biāo)依舊為 5妻味,但是 key2 由于 擴(kuò)容正压, 得出的角標(biāo) 加了 16,即21责球, 16是oldTab的length焦履,再來(lái)看e.hash & oldCap,oldCap.length即n 本身為 0000 0000 0000 0000 0000 0000 0001 0000 雏逾,這個(gè)位與運(yùn)算可以得出擴(kuò)容后哪些key 在 擴(kuò)容新增位時(shí)1嘉裤,哪些是0,一個(gè)位運(yùn)算替換了rehash過(guò)程栖博,是不是得給100個(gè)??????...??屑宠,大概擴(kuò)容的過(guò)程如下:
image.png

線程安全問(wèn)題

JDK1.7 HashMap在多線程的擴(kuò)容時(shí)確實(shí)會(huì)出現(xiàn)循環(huán)引用,導(dǎo)致下次get時(shí)死循環(huán)的問(wèn)題仇让,具體可以參考HashMap死循環(huán)問(wèn)題典奉。很多文章在說(shuō)到死循環(huán)時(shí)都以JDK1.7來(lái)舉例,其實(shí)JDK1.8的優(yōu)化已經(jīng)避免了死循環(huán)這個(gè)問(wèn)題丧叽,但是會(huì)造成數(shù)據(jù)丟失問(wèn)題卫玖,下面我舉個(gè)例子(需要對(duì)應(yīng)上邊resize的下半部分代碼):

創(chuàng)建 thread1 和 thread2 去添加數(shù)據(jù),此時(shí)都在resize踊淳,兩個(gè)線程分別創(chuàng)建了兩個(gè)newTable假瞬,并且thread1在table = newTab;處調(diào)度到thread2(沒(méi)有給table賦值),等待thread2擴(kuò)容之后再調(diào)度回thread1,注意笨触,擴(kuò)容時(shí)oldTab[j] = null; 也就將 oldTable中都清掉了懦傍,當(dāng)回到thread1時(shí),將table指向thread1的newTable芦劣,但訪問(wèn)oldTable中的元素全部為null粗俱,所以造成了數(shù)據(jù)丟失。

putVal()方法

put方法其實(shí)調(diào)用了putVal(參數(shù)onlyIfAbsent表示如果為true虚吟,若put的位置已經(jīng)有value寸认,則不修改,putIfAbsent方法中傳true)串慰,這個(gè)方法的重點(diǎn)在于 TREEIFY_THRESHOLD 這個(gè)變量偏塞,如果鏈表長(zhǎng)度 >= TREEIFY_THRESHOLD - 1 ,則調(diào)用 treeifyBin 方法邦鲫, 從它的注釋上可以看出灸叼,這個(gè)方法會(huì)把這條鏈所有的Node變?yōu)榧t黑樹(shù)結(jié)構(gòu)。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> 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<K,V> 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<K,V>)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<K,V>[] tab, int hash) {
      int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)  // 如果 tab.length 小于 64庆捺, 則只進(jìn)行 resize
            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);  // 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);   // 紅黑樹(shù)轉(zhuǎn)換過(guò)程
        }
}
entrySet()遍歷

我們?cè)诒闅vHashMap的時(shí)候都會(huì)使用 map.entrySet().iterator()古今,看下這個(gè) iterator 是什么:

public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        ...
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
        ...
    }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

    abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> 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 < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> 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一樣滔以,還是modCountexpectedModCount的問(wèn)題捉腥,expectedModCountiterator構(gòu)造時(shí)賦的值,等于當(dāng)時(shí)的modCount你画,所以如果已經(jīng)生成了iterator抵碟,如果擅自使用map.put()等操作,會(huì)使modCount變化坏匪,導(dǎo)致expectedModCount != modCount拟逮,會(huì)拋出ConcurrentModificationException

結(jié)尾

好了剥槐,以上除了紅黑樹(shù)唱歧,HashMap中的我認(rèn)為的核心內(nèi)容至此就說(shuō)完了宪摧,可以看出JDK一直在許多細(xì)節(jié)上不斷地在做優(yōu)化粒竖,作為我們,還是需要不斷地修煉几于,去發(fā)現(xiàn)這些代碼中的驚艷之處蕊苗!

參考

https://tech.meituan.com/java-hashmap.html
http://blog.csdn.net/xuefeng0707/article/details/40797085

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市沿彭,隨后出現(xiàn)的幾起案子朽砰,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞧柔,死亡現(xiàn)場(chǎng)離奇詭異漆弄,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)造锅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門撼唾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人哥蔚,你說(shuō)我怎么就攤上這事倒谷。” “怎么了糙箍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵渤愁,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我深夯,道長(zhǎng)抖格,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任咕晋,我火速辦了婚禮他挎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捡需。我一直安慰自己办桨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布站辉。 她就那樣靜靜地躺著呢撞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪饰剥。 梳的紋絲不亂的頭發(fā)上殊霞,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音汰蓉,去河邊找鬼绷蹲。 笑死,一個(gè)胖子當(dāng)著我的面吹牛顾孽,可吹牛的內(nèi)容都是我干的祝钢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼若厚,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼拦英!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起测秸,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤疤估,失蹤者是張志新(化名)和其女友劉穎灾常,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體铃拇,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钞瀑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了慷荔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仔戈。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拧廊,靈堂內(nèi)的尸體忽然破棺而出监徘,到底是詐尸還是另有隱情,我是刑警寧澤吧碾,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布凰盔,位于F島的核電站,受9級(jí)特大地震影響倦春,放射性物質(zhì)發(fā)生泄漏户敬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一睁本、第九天 我趴在偏房一處隱蔽的房頂上張望尿庐。 院中可真熱鬧,春花似錦呢堰、人聲如沸抄瑟。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)皮假。三九已至,卻和暖如春骂维,著一層夾襖步出監(jiān)牢的瞬間惹资,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工航闺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留褪测,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓潦刃,卻偏偏與公主長(zhǎng)得像侮措,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子福铅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345