HashMap1.8插入元素、擴容部分源碼分析以及線程不安全的原因

先看幾個關鍵的屬性

//默認數組初始化長度爲16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//最大長度
static final int MAXIMUM_CAPACITY = 1 << 30;
//負載因子,擴容的閾值,比如說16*0.75=12,當數組使用了12的時候就會觸發擴容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當鏈表長度爲8的時候轉爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//當節點小於6的時候就轉爲鏈表
static final int UNTREEIFY_THRESHOLD = 6;
//最小樹形化容量閾值
static final int MIN_TREEIFY_CAPACITY = 64;

接下來看put的過程

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//hash算法,高16位跟低16位進行異或運算,這樣目的是使結果更加隨機性,儘可能使數據均勻分佈
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

put的時候調用的是putVal方法,源碼如下

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)
            //在第一次put的時候進行初始化數組,只有使用的時候才初始化大小,體現的是懶加載的思想,也可以節省內存空間
            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;//如果key值相等,則把舊的值覆蓋
            else if (p instanceof TreeNode)//如果是紅黑樹則調用紅黑樹的put方法
                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) // 大於等於8的時候轉爲紅黑樹,binCount是從0開始的,所以要減一
                            treeifyBin(tab, hash);
                        break;
                    }
                    //插入的key跟鏈表已有的key重複,則跳出循環
                    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)//當容量大於threshold,比如第一次調用resize()初始化的賦給的值12,即16*0.75
            resize();//進行擴容
        afterNodeInsertion(evict);
        return null;
    }

簡單解釋下put的時候主要做的操作
1、當數組table爲空的時候,先調用resize()進行初始化, 根據(n - 1) & hash,這樣就找到該key的存放位置
2、如果數組的key已經存在,則用新值替換舊值
3、如果當前數組節點table[i]已經存在值,則根據當前節點的類型看是紅黑樹還是鏈表,如果是紅黑樹則調用putTreeVal,
4、如果是鏈表則對鏈表進行循環遍歷,找到末尾進行插入(尾插法)。其中鏈表過長還會轉爲紅黑樹
5、符合擴容條件就進行擴容
ps:每次put操作的時候返回的都是上一次插入的數據,如果節點爲空,返回null,如果是覆蓋操作則返回的是舊值
在這裏插入圖片描述

將鏈表轉爲紅黑樹

    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)
            resize();//如果數組爲空或者數組的長度小於64,優先進行擴容
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                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);
        }
    }

接下來我們看下擴容的操作,從上面分析可知擴容會發生在兩個地方
1、轉爲紅黑樹的時候
2、當數組put的元素達到閾值的時候

    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; // 向左位移一位,也就是變爲原來的2倍
        }
        else if (oldThr > 0) //原來臨界值不爲空但原來容量爲空的情況,則把容量設置爲臨界值(把原來設置的值全部刪除了,這個時候oldCap==0,但是oldThr>0)
            newCap = oldThr;
        else {
          //第一次初始化
            newCap = DEFAULT_INITIAL_CAPACITY;//16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
        }
        if (newThr == 0) {//newThr爲0時,按公式進行計算給newThr一個值
            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];//創建新的數組,這個數組第一次是16,後續就是32,64,128。。。
        table = newTab;
        if (oldTab != null) {//進行元素遷移
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;//臨時節點
                if ((e = oldTab[j]) != null) {//將原來的節點賦給e,然後把原來的節點置空
                    oldTab[j] = null;
                    if (e.next == null)
                        //說明不是鏈表,計算新數組的下標,跟第一次put的時候一樣
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //節點是樹,按照樹的方式打散
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { //鏈表的方式
                        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;//尾節點next屬性置空
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;//尾節點next屬性置空
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

resize()主要做了以下幾件事
1、如果是第一次初始化的時候,創建一個長度爲16的數組
2、如果原數組容量不爲空,則創建一個新的數組,該數組的長度爲原來的2倍,閾值也爲原來的2倍(都向左位移一位)
3、遍歷舊數組,根據hash跟(新數組的容量-1)異或計算節點在新數組的位置,然後進行賦值
4、如果節點是紅黑樹就按照紅黑樹的方式進行存放
5、如果是鏈表,則存放的位置有兩種可能,要麼就是在原來的位置,要麼就是在(原來的位置+舊數組的容量)
比如原來鏈表是在8,則到新的數組後要麼就還是在8,要麼就是8+16,這個要看(e.hash & oldCap)的結果是否爲0

HashMap線程不安全的表現
通過上面的源碼分析,我們可以發現HashMap線程不安全主要表現在以下幾個情形:

1、如下圖,如果線程A和線程B同時進行put操作,剛好這兩條不同的數據hash值一樣,並且該位置數據爲null,假設一種情況,線程A判斷完畢後還未進行數據賦值時掛起,而線程B正常執行,然後線程A獲取CPU時間片,此時線程A不用再進行hash判斷了,這時線程A會把線程B插入的數據給覆蓋,鏈表也類似
在這裏插入圖片描述
2、在擴容的時候,在執行賦值的時候這個時候假如有線程在賦值前插入數據,那麼也是會被覆蓋的,因爲這個時候table已經指向了newTab了,別的線程插入的時候就是往擴容後的數組插入了
在這裏插入圖片描述
在這裏插入圖片描述

3、如果在執行擴容的時候,剛好執行到 table = newTab;這時候某個線程就立刻想刪除以前插入的某個元素,你會發現刪除不了,因爲table指向了新數組,而這時候新數組還沒有任何數據。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章