hashmap相關(guān)問題

概述

java8對(duì)java7中hashmap的實(shí)現(xiàn)進(jìn)行了一部分修改涩堤,最大的修改在于利用了紅黑樹,jdk7使用數(shù)組+鏈表實(shí)現(xiàn)hashmap躺率,jdk8使用數(shù)組+鏈表+紅黑樹實(shí)現(xiàn)hashmap,之所以jdk8要引入紅黑樹,因?yàn)閖dk7的單向鏈表可以無限延伸寝贡,時(shí)間復(fù)雜度取決于鏈表的長(zhǎng)度,為O(n)值依,為了降低這部分開銷圃泡,jdk8中,當(dāng)單向鏈表中元素達(dá)到8個(gè)的時(shí)候愿险,會(huì)將鏈表轉(zhuǎn)換為紅黑樹颇蜡,這些位置的查找的時(shí)間復(fù)雜度降低為O(logN)

java7使用Entry描述hashmap中的每個(gè)數(shù)據(jù)節(jié)點(diǎn),java8在沒生成紅黑樹的時(shí)候辆亏,使用node來描述鏈表元素风秤,當(dāng)轉(zhuǎn)換紅黑樹的時(shí)候,使用treenode描述鏈表元素

哈希表

哈希表扮叨,又稱散列表缤弦,是一種數(shù)據(jù)結(jié)構(gòu),一個(gè)哈希表包含一個(gè)數(shù)組彻磁,通過特殊的key來訪問數(shù)組中元素碍沐,哈希表主要思想是通過一個(gè)哈希函數(shù),在key映射的位置去尋找存放值的地方

如下圖字典:尋找安存放的位置衷蜓,通過hash函數(shù)f(安)計(jì)算出an累提,從而放在第二位,哈希沖突:f(按)=an磁浇,追加在安后面斋陪,哈希沖突無法避免

哈希表的特性決定了哈希表具有較高性能,大多數(shù)查找或插入元素時(shí)間復(fù)雜度都為O(1)扯夭,時(shí)間主要花在計(jì)算hash值上鳍贾,如果使用的hash函數(shù)不夠好,會(huì)導(dǎo)致大量hash值映射在同一個(gè)地址上交洗,這樣哈希表就會(huì)退化成鏈表骑科,時(shí)間復(fù)雜度變O(n),效率低下

jdk8版本hashmap結(jié)構(gòu)

jdk8版本hashmap源碼

基本屬性

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 用于擴(kuò)容构拳,不會(huì)說到16才擴(kuò)容咆爽,沒到16的時(shí)候就會(huì)梁棠,這個(gè)閾值就是加載因子乘以容量,即12的時(shí)候就會(huì)擴(kuò)容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 變成紅黑樹的閾值=8
    static final int TREEIFY_THRESHOLD = 8;
    transient Node<K,V>[] table;

put(K key, V value)

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    static final int hash(Object key) {
        int h;
        //此處為了降低哈希沖突的幾率斗埂,在這里不直接返回hashCode而是分下面幾步
        //1 獲取key的HashCode
        //2 將獲取到的hashCode向右移16位
        //3 移動(dòng)結(jié)果和原來的key的hashCode異或運(yùn)算 (相同的異或?yàn)?符糊,不相同異或?yàn)?)
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

元素在數(shù)組中存放的位置是由下面的i=(n-1)&hash決定的,這行代碼摘自putVal()呛凶,n為當(dāng)前數(shù)組的大小男娄,i為當(dāng)前元素存放的索引

測(cè)試代碼
HashMap map=new HashMap(1)
帶參數(shù)初始化

tableSizeFor結(jié)果為1,this.threshold擴(kuò)容閾值初始化為1漾稀,然后執(zhí)行下面代碼map.put("key1",111)模闲,發(fā)現(xiàn)tab是null,開始準(zhǔn)備調(diào)用擴(kuò)容方法初始化table

擴(kuò)容的時(shí)候崭捍,oldCap=0尸折,oldThr=1,newCap=newThr初始值為0殷蛇,在經(jīng)過else if完后实夹,newCap=1,在經(jīng)過if(newThr==0)后粒梦,newThr=(int)ft亮航,而ft=0.75,取int后未0匀们,即newThr新的擴(kuò)容閾值為0塞赂,只要table中元素超過0,就重新調(diào)用本方法昼蛀,然后初始化table,初始化完后返回到putVal方法

在putVal方法中圆存,首先將要放入的key1元素放入進(jìn)去

然后到最下邊叼旋,開始判斷是否需要擴(kuò)容,putVal放入一個(gè)元素后size=1沦辙,超過擴(kuò)容閾值0夫植,開始擴(kuò)容

然后resize部分代碼上邊部分如下,oldCap=1,oldThr=0,newCap在下面的1中變成2油讯,newThr經(jīng)過計(jì)算2*0.75=1.5變成1详民,然后完成新的table的初始化,準(zhǔn)備開始遷移

putVal


下面為變換紅黑樹的條件判斷以及判斷是否有節(jié)點(diǎn)的key跟待插入的key相同的邏輯

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 創(chuàng)建變量引用
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //先用局部變量拷貝一份成員變量陌兑,然后對(duì)局部變量判斷沈跨,另一個(gè)局部變量也對(duì)剛剛被賦值的局部變量判斷
        if ((tab = table) == null || (n = tab.length) == 0)
            // 數(shù)組沒初始化就進(jìn)行初始化
            n = (tab = resize()).length;
        // 尋址算法算出應(yīng)該放的位置沒有元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 直接創(chuàng)建元素節(jié)點(diǎn)放入
            tab[i] = newNode(hash, key, value, null);
        else {
            // e指針保存如果之前該key已經(jīng)有值了的舊值,k保存舊值的key
            Node<K,V> e; K k;
            // 如果要插入的位置的哈希跟要插入的key的hash結(jié)果相同并且【 key相同或者key不為空兔综,但是key和那個(gè)位置的key equals】

            // 注意:***下面是if elseif else 三個(gè)分支6隽荨D辍!
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) {
                // 認(rèn)為是同一個(gè)key的插入涧窒,則用局部變量e取出這個(gè)舊的節(jié)點(diǎn)
                e = p;
            } else if (p instanceof TreeNode)   // 如果尋址算法算出的位置上的該節(jié)點(diǎn)是紅黑樹節(jié)點(diǎn)
                // 用局部變量e取出這個(gè)舊的紅黑樹節(jié)點(diǎn)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 數(shù)組上的尋址算法算出的位置上該節(jié)點(diǎn)的key和要寫入的key不同心肪,走了上面兩個(gè)if以后,這里必定是鏈表節(jié)點(diǎn)
                for (int binCount = 0; ; ++binCount) {
                    // 用e記錄節(jié)點(diǎn)p的下一個(gè)元素纠吴,如果不存在
                    if ((e = p.next) == null) {
                        // 直接將新元素插入作為p元素的后繼節(jié)點(diǎn)
                        p.next = newNode(hash, key, value, null);
                        // binCount=0的時(shí)候硬鞍,有2個(gè)節(jié)點(diǎn),p節(jié)點(diǎn)和新插入的節(jié)點(diǎn)
                        // binCount=7的時(shí)候戴已,有9個(gè)節(jié)點(diǎn)固该,p節(jié)點(diǎn)+p后面的原本節(jié)點(diǎn)+新插入的節(jié)點(diǎn)
                        // binCount與新插入的節(jié)點(diǎn)無關(guān)系!9Ф浮蹬音! 原來鏈表上有8個(gè)節(jié)點(diǎn),現(xiàn)在先插入以后再判斷轉(zhuǎn)化為紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 轉(zhuǎn)化為紅黑樹
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 說明p節(jié)點(diǎn)后面還有節(jié)點(diǎn)休玩,e是p下一個(gè)節(jié)點(diǎn)著淆,如果e就是要插入的key的相同的key值,跳出
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;

                    // 繼續(xù)進(jìn)行循環(huán)拴疤,將p的下一個(gè)節(jié)點(diǎn)e作為新的p永部,e指向更下一個(gè)節(jié)點(diǎn)
                    p = e;
                }
            }

            // 如果e是null,說明沒有找到相同key的節(jié)點(diǎn) e是exists
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 如果允許覆蓋或者舊值為null  (hashmap是允許key呐矾,value都可以是null的)
                if (!onlyIfAbsent || oldValue == null)
                    // 將新的值賦值給那個(gè)節(jié)點(diǎn)
                    e.value = value;
                afterNodeAccess(e); // hashmap自身并未實(shí)現(xiàn)該方法細(xì)節(jié)苔埋,子類實(shí)現(xiàn)
                return oldValue;    // 將之前的同key的節(jié)點(diǎn)的舊value返回
            }
        }
        ++modCount;
        // 如果新加入元素以后的值大于擴(kuò)容閾值
        if (++size > threshold)
            // 進(jìn)行擴(kuò)容
            resize();
        afterNodeInsertion(evict);  // hashmap自身并未實(shí)現(xiàn)該方法細(xì)節(jié),子類實(shí)現(xiàn)
        return null;
    }

resize

擴(kuò)容過程主要完成了map的初始化以及容量到達(dá)閾值的時(shí)候創(chuàng)建新的數(shù)組蜒犯,并將舊數(shù)組數(shù)據(jù)遷移

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        // 后面判斷都是用局部變量去判斷的
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 舊的擴(kuò)容閾值
        int oldThr = threshold;
        // 聲明新的數(shù)組容量和新的擴(kuò)容閾值
        int newCap, newThr = 0;
        // 如果老的容量>0
        if (oldCap > 0) {
            // 如果老的數(shù)組容量已經(jīng)達(dá)到了最大容量了
            if (oldCap >= MAXIMUM_CAPACITY) {
                // 擴(kuò)容閾值修改為2^32
                threshold = Integer.MAX_VALUE;
                // 不進(jìn)行擴(kuò)容直接返回老的容量
                return oldTab;
            }
            // 否則组橄,判斷將老的數(shù)組容量放大2倍以后如果沒超過最大容量且老的容量大于等于初始的默認(rèn)的初始容量16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 符合條件則將新的閾值變?yōu)槔祥撝档?倍
                newThr = oldThr << 1; // double threshold

            // 這里必要條件:老的容量小于最大容量2的30次冪
            // 這里選擇條件:擴(kuò)容2倍后大于等于最大容量 或者老的數(shù)組容量小于默認(rèn)的容量16(如5,適配后變成8小于16)罚随,newThr這時(shí)候仍然為0



        }
        // 必要條件:oldCap=0
        // 可能有兩種情況玉工,一種是oldThreshold=0(不帶初始容量初始化),一種是大于0(帶初始容量初始化)
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 即帶初始容量參數(shù)初始化(new HashMap(initCapacity))淘菩。oldThr在tableSizeFor被賦值遵班,直接將他賦值給新的*容量*。oldThr只是用來配置參數(shù)潮改,那時(shí)候還沒有實(shí)例化數(shù)組狭郑,這里才真正實(shí)例化,要用到上面的參數(shù)汇在。這時(shí)候newCap=oldThr翰萨,newThr=0(代碼頭部初始化的)
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 說明為不帶參數(shù)初始化(new HashMap())
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // 上面主要是為了堆newCap和newThr賦值,如果newThr沒有值說明走了if中的else if的else  或者 走了外圍的else if(oldThr>0)
        /*
即可能存在 :新擴(kuò)容后的容量大于最大容量 或者 

老的容量小于默認(rèn)初始容量16(最有可能) 
或者 oldThr>0
*/
        // newCap已經(jīng)在if的elseif分支賦值了
        if (newThr == 0) {
            // 假設(shè)老的容量為8糕殉,加載因子是0.75缨历,則ft=6以蕴,newCap=1,則ft=0.75
            float ft = (float)newCap * loadFactor;
            // 如果新的容量小于最大容量并且新計(jì)算的float類型的擴(kuò)容閾值ft小于 float類型的最大容量辛孵,則新的擴(kuò)容閾值為該int類型的擴(kuò)容閾值


/**
帶參數(shù)初始化容量1丛肮,則首先執(zhí)行完:newCap=1,newThr=0魄缚,然后回到putValue方法宝与,放入要添加的元素以后發(fā)現(xiàn)大于擴(kuò)容閾值,開始擴(kuò)容
*/
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})

        // 創(chuàng)建新的數(shù)組用新的容量
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

        // 這里讓table指向新的table的內(nèi)存地址冶匹,但是臨時(shí)變量oldTable還是指向了舊的數(shù)組的內(nèi)存地址
        table = newTab;
        // 如果為初始化這里已經(jīng)結(jié)束
        if (oldTab != null) {

            // 這里說明老的數(shù)組存在习劫,則遍歷
            for (int j = 0; j < oldCap; ++j) {

                // 游走指針,記錄每個(gè)被遍歷的元素
                Node<K,V> e;
                // 如果舊數(shù)組第一個(gè)元素存在
                if ((e = oldTab[j]) != null) {
                    // 將舊數(shù)組第一個(gè)位置置空
                    oldTab[j] = null;
                    // 如果舊數(shù)組第一個(gè)元素所在的鏈表沒有后面的元素了
                    if (e.next == null)
                        // 將記錄的這個(gè)舊數(shù)組第一個(gè)元素使用 尋址算法嚼隘,再計(jì)算一次他的位置诽里,只能是原位置或者加上oldCapacity
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果舊數(shù)組第一個(gè)元素所在鏈表后面有元素,并且是紅黑樹節(jié)點(diǎn)
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        // 說明為鏈表節(jié)點(diǎn)飞蛹,開始遍歷第j個(gè)元素的鏈表節(jié)點(diǎn)

                        // 聲明低鏈表頭尾指針
                        Node<K,V> loHead = null, loTail = null;
                        // 聲明高鏈表頭尾指針
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 節(jié)點(diǎn)哈希值 和 舊的數(shù)組容量 與的結(jié)果 只能是0和舊的容量谤狡,從0開始一部分,從oldCap一部分卧檐, 又從0開始一部分
                            // 需要將此鏈表拆成兩個(gè)鏈表墓懂,放到新的數(shù)組中,并且保留原來的先后順序
                            // loHead霉囚、loTail 對(duì)應(yīng)一條鏈表捕仔,hiHead、hiTail 對(duì)應(yīng)另一條鏈表

                            // 低位鏈表
                            if ((e.hash & oldCap) == 0) {
                                // 如果低位鏈表此時(shí)沒有尾結(jié)點(diǎn)
                                if (loTail == null)
                                    // 將當(dāng)前節(jié)點(diǎn)設(shè)置為低位鏈表的頭結(jié)點(diǎn)
                                    loHead = e;
                                else
                                    // 將低位鏈表之前的尾結(jié)點(diǎn)的next指針指向當(dāng)前節(jié)點(diǎn)
                                    loTail.next = e;
                                // 將當(dāng)前節(jié)點(diǎn)設(shè)置為低位鏈表的尾結(jié)點(diǎn)
                                loTail = e;
                            }
                            // 高位鏈表盈罐,與的結(jié)果是舊的數(shù)組cap榜跌,操作同上,就是完成一個(gè)高位鏈表的初始化
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);

                        // 某個(gè)節(jié)點(diǎn)上的高位鏈表和低位鏈表初始化完以后盅粪,將低位鏈表的頭部放到新鏈表的j角標(biāo)位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 將高位鏈表的頭部放到新鏈表的j+舊的數(shù)組容量的尋址位置
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

面試題

1. 為什么定義hashmap初始值是16是2的n次冪斜做,擴(kuò)容的時(shí)候新數(shù)組大小要是2的n次冪

核心:尋址算法

i是數(shù)組下標(biāo),n是數(shù)組長(zhǎng)度湾揽,hash是要插入的entry的key的hash值
i =(n-1)&hash 

2的次冪-1永遠(yuǎn)得到的結(jié)果的二進(jìn)制位最后面都是1,如

2-1
0000 0000 0000 0001
4-1
0000 0000 0000 0011
8-1
0000 0000 0000 0111
16-1
0000 0000 0000 1111
32-1
0000 0000 0001 1111

我們接下來按照如果數(shù)組長(zhǎng)度是2的次冪和不是2的次冪來做尋址算法比較笼吟,假如我們又4個(gè)entry等待插入库物,entry的key的hash計(jì)算結(jié)果分別為:

9 0000 0000 0001 1001
10 0000 0000 0001 1010
11 0000 0000 0001 1011
12 0000 0000 0001 1100
  • 如果長(zhǎng)度是2的次冪,如默認(rèn)的數(shù)組長(zhǎng)度16贷帮,n-1=16-1=15 = 0000 0000 0000 1111
  • 如果長(zhǎng)度不是2的次冪戚揭,如長(zhǎng)度為10,n-1=9=0000 0000 0001 1001

可以發(fā)現(xiàn)如果數(shù)組的長(zhǎng)度不使用2的n次冪的話撵枢,經(jīng)過尋址算法民晒,得到的結(jié)果會(huì)出現(xiàn)很多重復(fù)精居,增加了哈希碰撞的幾率。因此HashMap的初始容量是2的n次冪潜必。
下面我們看為什么擴(kuò)容也需要是2的冪次數(shù)靴姿,滿足2的冪就是相當(dāng)于每次擴(kuò)容都是翻倍

假設(shè)現(xiàn)在擴(kuò)容為長(zhǎng)度32,n-1=31=0000 0000 0001 1111則尋址算法結(jié)果為

可以看到磁滚,擴(kuò)容時(shí)在重新計(jì)算下標(biāo)位置時(shí)佛吓,只有兩種情況,一種是下標(biāo)不變垂攘,另一種是下標(biāo)變?yōu)椋涸聵?biāo)位置+擴(kuò)容前容量维雇,而上圖很明顯就是在擴(kuò)容器的與的二進(jìn)制結(jié)果的第一位加了1,等于是在原有的結(jié)果基礎(chǔ)上加了2的4次冪即擴(kuò)容前的容量16.這樣擴(kuò)容后節(jié)點(diǎn)移動(dòng)相對(duì)較少晒他,可以提高性能

總結(jié):
HashMap計(jì)算添加元素的位置時(shí)吱型,使用的位運(yùn)算,這是特別高效的運(yùn)算陨仅;另外津滞,HashMap的初始容量是2的n次冪,擴(kuò)容也是2倍的形式進(jìn)行擴(kuò)容掂名,是因?yàn)槿萘渴?的n次冪据沈,可以使得添加的元素均勻分布在HashMap中的數(shù)組上,減少hash碰撞饺蔑,避免形成鏈表的結(jié)構(gòu)锌介,使得查詢效率降低!

2. hashmap初始化的時(shí)候可以指定數(shù)組長(zhǎng)度猾警,那是怎么保證輸入的值是2的n次冪

主要是通過如下代碼完成對(duì)輸入數(shù)組長(zhǎng)度轉(zhuǎn)化為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 < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

這一系列的無符號(hào)右移操作和按位或運(yùn)算返回的什么結(jié)果孔祸,我們可以通過一個(gè)測(cè)試來說明

   public static void main(String[] args) throws Exception {
        HashMap map = new HashMap();
        Method tableSizeFor = map.getClass().getDeclaredMethod("tableSizeFor", int.class);
        tableSizeFor.setAccessible(true);
        List<Integer> list = Lists.newArrayList(3,4,15,16,17,31,32,33);
        list.forEach(e-> {
            try {
                System.out.println(tableSizeFor.invoke(map,e));
            } catch (IllegalAccessException | InvocationTargetException e1) {
                e1.printStackTrace();
            }
        });
    }

打印結(jié)果如下:

可以看出是用距離傳入的數(shù)組正向長(zhǎng)度最近的2次冪來替換該長(zhǎng)度

3. 什么時(shí)候擴(kuò)容以及加載因子為什么是0.75

    /**
     * The next size value at which to resize (capacity * load factor).
     *
    int threshold;

threshold是擴(kuò)容閾值,達(dá)到threshold就擴(kuò)容发皿,值為數(shù)組長(zhǎng)度 x 加載因子
至于加載因子為什么是0.75是因?yàn)椋涸诶硐肭闆r下崔慧,使用隨機(jī)哈希碼,在加載因子為0.75的情況下穴墅,節(jié)點(diǎn)出現(xiàn)在頻率在Hash桶(表)中遵循參數(shù)平均為0.5的泊松分布

4. 講述一下java8的hashmap的擴(kuò)容過程

根據(jù)我們?cè)诘谝坏烂嬖囶}中的結(jié)論 驗(yàn)證了 hashmap擴(kuò)容方法的注釋

當(dāng)超過限制的時(shí)候會(huì)resize惶室,然而又因?yàn)槲覀兪褂玫氖?次冪的擴(kuò)展(指長(zhǎng)度擴(kuò)為原來2倍),所以玄货,元素的位置要么是在原位置皇钞,要么是在原位置再移動(dòng)2次冪的位置。

我們?cè)跀U(kuò)充HashMap的時(shí)候松捉,不需要重新計(jì)算hash夹界,只需要看看原來的hash值新增的第一個(gè)bit位“溃看那個(gè)bit是1還是0就好了可柿,是0的話索引沒變鸠踪,是1的話索引變成“原索引+oldCap”「闯猓可以看看下圖為16擴(kuò)充為32的resize示意圖:

這個(gè)設(shè)計(jì)非常的巧妙营密,既省去了重新計(jì)算hash值的時(shí)間,而且同時(shí)永票,由于新增的1bit是0還是1可以認(rèn)為是隨機(jī)的卵贱,因此resize的過程,均勻的把之前的沖突的節(jié)點(diǎn)分散到新的bucket了

5. 講述一下hashmap中用到的鏈表頭插法與尾插法

jdk1.7插入元素到單鏈表中采用頭插法侣集,jdk1.8采用的是尾插法键俱。

jdk1.7 插入鏈表的頭部,有一種看法是新插入的數(shù)據(jù)被查詢的概率比較大,插入到頭部查詢相對(duì)比較快. 但是在多線程環(huán)境中擴(kuò)容可能會(huì)造成循環(huán)鏈表,導(dǎo)致CPU100%
jdk1.8 改進(jìn):采用尾插法,在擴(kuò)容時(shí)不用重新計(jì)算hash值,元素索引值的變換是有規(guī)律的.

1.7版本的死循環(huán)問題

擴(kuò)容的時(shí)候,會(huì)遍歷原Entry數(shù)組世分,把所有的Entry重新Hash到新數(shù)組编振。為什么要重新Hash呢?因?yàn)殚L(zhǎng)度擴(kuò)大以后臭埋,Hash的規(guī)則也隨之改變踪央。

讓我們回顧一下Hash公式:
index = HashCode(Key) & (Length - 1)
當(dāng)原數(shù)組長(zhǎng)度為8時(shí),Hash運(yùn)算是和111B做與運(yùn)算瓢阴;新數(shù)組長(zhǎng)度為16畅蹂,Hash運(yùn)算是和1111B做與運(yùn)算。Hash結(jié)果顯然不同

正常的rehash過程:
假如使用的hash算法就是簡(jiǎn)單的用key mod 一下也就是數(shù)組的長(zhǎng)度
最上面的是old hash 表荣恐,其中的Hash表的size=2, 所以key = 3, 7, 5液斜,在mod 2以后都沖突在table[1]這里了
接下來的三個(gè)步驟是Hash表 resize成4,然后所有的<key,value> 重新rehash的過程

并發(fā)下的rehash
有兩個(gè)線程叠穆,線程A和線程B少漆,線程A在執(zhí)行完Entry<K,V>next = e.next后被調(diào)度掛起,e指向key3硼被,next指向key7

然后線程B開始執(zhí)行示损,并且執(zhí)行完了整個(gè)rehash過程

線程A在線程B rehash后,指向了線程B重組后的原鏈表嚷硫,然后線程A開始執(zhí)行
【注意:之所以并發(fā)操作有問題检访,不是在線程內(nèi)部的newTable上,而是在線程A仔掸、B會(huì)操作oldTable脆贵,造成并發(fā)問題,因?yàn)榫€程B對(duì)oldTable中元素的指向的改變導(dǎo)致出現(xiàn)問題】

這里有個(gè)非常重要的地方嘉汰,擴(kuò)容后的newTable是方法參數(shù),是屬于線程私有W辞凇P场双泪!注意

線程A的rehash過程:

  1. 此時(shí)e指向key3,next指向key7密似,而key7的next因?yàn)榫€程B的rehash仍然指向key3【導(dǎo)致問題核心】焙矛,新的table表是線程A的,表中無元素
  2. 執(zhí)行e.next=newTable[i]=null残腌,讓key3的next指向null村斟,然后讓key3成為3位置的頭號(hào)節(jié)點(diǎn),然后next指針仍然是1步驟中指向key7抛猫,然后因?yàn)閑=next蟆盹,所以e指向key7
  3. e指向key7,next指針因?yàn)?步驟的原因仍然指向key3闺金,而key3的next是null逾滥,key7成為新的頭結(jié)點(diǎn),下一步e指向key3
  4. e指向key3败匹,next指向null寨昙,因?yàn)閑.next=newTable[i],所以key3的next指向key7掀亩,而key7的next一直指向key3舔哪,這時(shí)候環(huán)形鏈表就構(gòu)成了。又因?yàn)閑=next槽棍,next=null捉蚤,所以e指針和next指針均指向null
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市刹泄,隨后出現(xiàn)的幾起案子外里,更是在濱河造成了極大的恐慌,老刑警劉巖特石,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盅蝗,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡姆蘸,警方通過查閱死者的電腦和手機(jī)墩莫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逞敷,“玉大人狂秦,你說我怎么就攤上這事⊥凭瑁” “怎么了裂问?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我堪簿,道長(zhǎng)痊乾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任椭更,我火速辦了婚禮哪审,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘虑瀑。我一直安慰自己湿滓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布舌狗。 她就那樣靜靜地躺著叽奥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪把夸。 梳的紋絲不亂的頭發(fā)上而线,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音恋日,去河邊找鬼膀篮。 笑死,一個(gè)胖子當(dāng)著我的面吹牛岂膳,可吹牛的內(nèi)容都是我干的誓竿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谈截,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼筷屡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起簸喂,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤毙死,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后喻鳄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扼倘,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年除呵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了再菊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡颜曾,死狀恐怖纠拔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情泛豪,我是刑警寧澤稠诲,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布侦鹏,位于F島的核電站,受9級(jí)特大地震影響臀叙,放射性物質(zhì)發(fā)生泄漏种柑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一匹耕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧荠雕,春花似錦稳其、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盖文,卻和暖如春嘱蛋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背五续。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工洒敏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疙驾。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓凶伙,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親它碎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子函荣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355