Java HashMap源碼深度分析

?在java語言中,HashMap是一個非常重要的數據結構,它被廣泛用于存儲具有key-value映射關系的數據,HashMap提供了高效的數據結構來實現key-value的映射;從HashMap的設計與實現中,我們可以學到很多巧妙的計算機思維,對我們日常工作中進行編碼及方案設計存在很高的參考價值,學習和掌握HashMap就成了非常有必要的一件事情了。
學習HashMap,有這么幾個關鍵問題需要搞明白:

  • HashMap的數據結構是什么樣的,不同jdk版本架構是如何的;

  • HashMap關鍵屬性,屬性的含義及如何設置等問題;

  • HashMap的key-value插入流程是怎樣的;

  • HashMap的數據查詢流程是怎樣的;

  • HashMap的擴容機制是怎么工作的;

  • HashMap的數據更新流程是什么樣的(比如:刪除);

  • HashMap的并發問題產生原因及正確的并發用法(比如并發環境下如何產生cpu 100%);

搞明白這幾個關鍵問題,HashMap就算是掌握得差不多了,下面,從源碼級來分析一下這些關鍵問題的答案是什么。

一、 HashMap的數據結構


HashMap數據結構

HashMap的數據結構由數組+鏈表組成,從java 8開始,會新增紅黑樹這種數據結構,也就是在java8中,鏈表超過一定長度后就會變為紅黑樹。

在下文的分析中,如果不是特別說明,都是指的是java 8中的HashMap。

二 、HashMap關鍵屬性


有幾個關鍵的屬性需要我們知道:

  /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默認的table的大小,table的大小只能是2的冪次方,這和HashMap的哈希槽尋址算法存在著很大的關系,當然,HashMap執行resize的時候也會得益于這個table數組的冪次方大小的特性,這些問題稍后再分析;

 /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

table的最大長度;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

默認的負載因子,負載因子用于擴容,當HashMap中的元素數量大于:capacity * loadFactor后,HashMap就會執行擴容流程。

 /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

當鏈表的長度超過一定長度之和,鏈表就會被升級為紅黑樹,用于解決因為哈希碰撞非常嚴重的情況下的數據查詢效率低下問題,最壞情況下,如果沒有引入紅黑樹的情況下,get操作的時間復雜度將達到O(N),而引入紅黑樹,最壞情況下get操作的時間復雜度為O(lgN);8是默認的鏈表樹化閾值,當某個hash槽內的鏈表的長度超過8之后,這條鏈表將演變為紅黑樹。

 /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

紅黑樹也有可能會降級為鏈表,當在resize的時候,發現hash槽內的結構為紅黑樹,但是紅黑樹的節點數量小于等于6的時候,紅黑樹將降級為鏈表。

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

鏈表升級為紅黑樹還需要一個條件,就是table的長度要大于64,否則是不會執行升級操作的,會轉而執行一次resize操作。

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

table和上文中的結構圖中的數組對應。

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

下次數組擴容閾值,這個值是通過:capacity * loadFactor計算出來的,每次擴容都會計算出下一次擴容的閾值,這個閾值說的是元素數量。

三、 HashMap數據插入流程


下面來跟著源碼來學習一下HashMap的put操作流程:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

首先需要注意的是hash這個方法:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

如果key是null,則hash的計算結構為0,這里就有一個關鍵信息,如果一個HashMap中存在key為null的entry,那么這個entry一定在table數組的第一個位置,這是因為hash計算的結果直接影響了數據插入的hash槽位置,這一點稍后再看。
如果key不為null,則會拿key對象的hashCode來進行計算,這里的計算稱為“擾動”,為什么要這樣操作呢?本質上是為了降低哈希碰撞的概率,這里需要引出HashMap中定位哈希槽的尋址算法。
HashMap的table數組容量只能是2的冪次方,這樣的話,2的冪次方的數有一個特性,那就是:hash & (len - 1) = hash % len,這樣,在計算entry的哈希槽位置的時候,只需要位運算就可以快速得到結果,不需要執行取模運算,位運算的速度非常快,要比取模運算快很多。
回到上面的問題,為什么要對key對象的hashCode執行擾動呢?因為計算哈希槽位置的時候需要和table數組的長度進行&運算,在絕大部分場景下,table數組的長度不會很大,這就導致hashCode的高位很大概率不能有效參加尋址計算,所以將key的hashCode的高16位于低16位執行了異或運算,這樣得到的hash值會均勻很多。
接下來繼續看put操作的流程:

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict
        // 1           
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        
        // 2
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 3    
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        
        // 4     
        else {
            // 5
            Node<K,V> e; K k;
            
            // 6
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            
            // 7    
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            
            // 8    
            else {
            
                // 9
                for (int binCount = 0; ; ++binCount) {
                
                    // 10
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        
                        // 11
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    
                    // 12 
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    
                    // 13
                    p = e;
                }
            }
            
            // 14
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
?
                return oldValue;
            }
        }
?
        // 15
        if (++size > threshold)
            resize();
?
        return null;
    }

下面根據注釋的編號來看一下對應位置的含義:

  • (1)這里主要關注tab和p兩個變量,tab是table數組的一個引用,p是當前拿到的Node引用,這個Node可能為null;

  • (2)這里將table賦值給了tab變量,并且判斷了tab數組是否為空,如果為空,表示是首次執行put操作,table還沒有被初始化出來,需要執行初始化操作,這里直接調用resize方法就可以完成初始化操作,關于resize,下一小節再重點分析。

  • (3)(n - 1) & hash計算出來的就是這個key對應的哈希槽,這個算法上面已經分析過,p變量拿到了當前哈希槽的頭節點,并進行了判斷,如果是null,則說明此時這個哈希槽內部沒有哈希沖突,直接創建一個新的Node插入這個槽即可;

  • (4)此時,說明p變量不為null,這個時候問題比較復雜,這個p可能是一個鏈表頭節點,也可能是一個紅黑樹根節點,這是結構上的可能性,接下來需要做的,就是判斷p代表的結構上是否存在即將要插入的key,如果存在,則說明節點已經存在,執行更新操作即可,如果不存在,則需要執行插入操作,看接下來的流程;

  • (5)同樣,e變量存儲的是代表存儲key的Node,可能為null,如果key壓根沒有被存儲過,那么e最終就是null,否則就是存儲key的Node的一個引用;

  • (6)這里是判斷哈希槽的頭結點是否是存儲key的節點,這是典型的判斷方法,先對比hash,然后對比key,對比key的時候要特別注意,除了使用“==”來進行比較,還使用了key對象的equals方法;如果判斷通過,則e就指向了已經存在的代表存儲key的Node;

  • (7)如果執行到這,說明p(哈希槽的頭結點)不是代表存儲key的Node,那么就要繼續后面的流程,這里首先判斷了一下p的結構,如果是TreeNode,說明p代表的是紅黑樹的頭結點,那就是要紅黑樹的節點插入方法putTreeVal來進行,關于紅黑樹,后續再仔細學習,本文點到為止。

  • (8)執行到這里,說明p代表的是一條鏈表的頭結點,需要在p這條鏈表中查找一下是否存在表示key的Node;

  • (9)開始迭代鏈表,來查找key;

  • (10)e此時表示的是p的next,如果e為null,說明鏈表迭代到了末尾,此時依然沒有發現key,則說明鏈表中根本不存在key節點,直接把key節點插入到末尾即可;

  • (11)這里有一個判斷,binCount如果超過了TREEIFY_THRESHOLD,則需要將鏈表升級為紅黑樹,通過treeifyBin方法來實現這個功能;

  • (12)如果e節點就是key節點,那么就可以結束了,e就是key節點的一個引用;

  • (13)p = e,就是將p向前移動,繼續判斷,簡單的鏈表迭代;

  • (14)如果此時e不為null,說明鏈表中存在key節點,那么本次put操作其實是一次replace操作;

  • (15)執行到這里,說明put操作插入了一個新的Node,如果插入后HashMap中的Node數量超過了本次擴容閾值,那么就要執行resize操作,resize操作將在下一小節詳細展開分析;

四、 HashMap擴容機制


擴容對于HashMap是一個很重要的操作,如果沒有擴容機制,因為有哈希碰撞的發生,會使得鏈表或者紅黑樹的節點數量過多,導致查詢效率較低。下面,就從源碼的角度來分析一下HashMap的擴容是如何完成的:

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
    
        // 1
        Node<K,V>[] oldTab = table;
        
        // 2
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        
        // 3 
        int oldThr = threshold;
        
        // 4
        int newCap, newThr = 0;
        
        // 5
        if (oldCap > 0) {
        
            // 6
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            
            // 7
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        
        // 8
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        
        // 9
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        
        // 10
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        
        // 11
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        
        // 12
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        
        // 13
        table = newTab;
        
        // 14
        if (oldTab != null) {
        
            // 15
            for (int j = 0; j < oldCap; ++j) {
            
                // 16
                Node<K,V> e;
                
                // 17
                if ((e = oldTab[j]) != null) {
                
                    // 18
                    oldTab[j] = null;
                    
                    // 19
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    
                    // 20
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    
                    // 21
                    else { // preserve order
                        
                        // 22
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            
                            // 23
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            
                            // 24
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        // 25
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        
                        // 26
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

resize操作很復雜,下面根據代碼注釋來看一下每個位置都在做什么:

  • (1)oldTab變量指向table,這個沒有特別難以理解;

  • (2)oldCap就是當前table的大小,也就是擴容前的table大小,table可能未初始化,則oldCap就是0;

  • (3、4)變量賦值,新老容量和擴容閾值;

  • (5、6、7)oldCap大于0,則說明table已經初始化過,擴容的時候,新的容量是老的table的兩倍,這里需要處理一下超過最大容量的問題,如果table數組已經達到最大了,那么就不要再繼續擴容了,生死有命富貴在天吧
    嘿嘿

  • (8)這種情況下是說在創建HashMap的時候指定了初始化大小,那新的table的容量就是當前的擴容閾值;

  • (9)執行到這里,說明table還沒創建,但是創建HashMap的時候沒有指定初始容量,那么本次其實就是執行初始化table的工作;

  • (10)計算新的擴容閾值;

  • (11、12、13)創建好新的數組,大小是原來的兩倍,并使用newTab表示;

  • (14)執行到這里,如果本次只是初始化table數組,那么其實resize的工作已經完成了,但是如果不是初始化table,而是執行正常的擴容操作,那么就需要執行數據遷移的工作,所謂數據遷移,就是將Node從原來的table中遷移到擴容出來的數組中;

  • (15)循環原來的table數組,逐個遷移數據;

  • (16)e變量用來表示當前遍歷到的Node;

  • (17)原table數組中當前哈希槽可能是空的,如果是空的,就說明沒有數據需要遷移,繼續處理下一個哈希槽就可以,如果當前哈希槽有節點,那么就需要對當前哈希槽內的數據執行遷移操作;

  • (18)e變量已經拿到了當前需要遷移的哈希槽的頭結點引用,執行oldTab[j] = null,就是為了減少引用,盡快讓垃圾得到回收;

  • (19)這種情況很簡單,當前需要遷移的哈希槽內部只有一個節點,那么就直接將該節點遷移到新的table中正確的位置就可以了;

  • (20)執行到這,表示哈希槽內是一顆紅黑樹,需要使用紅黑樹的節點遷移方法,這部分暫時不做分析;

  • (21)執行到這,說明需要遷移的是一條鏈表,下面就開始將這條鏈表上的節點遷移到新table中正確的位置上去;

  • (22)這里需要引出一個關鍵知識點,哈希槽數據遷移方案,得益于哈希數組的大小是2的冪次方這個特性,對于一個節點,在擴容后,它對應的哈希位置只可能存在兩種情況,要么還是當前位置(在新數組中),要么是當前位置+oldCap;這是為什么呢?來看一個例子:

并發數據遷移

我們假設擴容前數組長度為2,則擴容后數組的長度為4,原數組的數組下標為1的位置上存在一條鏈表需要遷移到新數組中去,這條鏈表長度為3,根據哈希槽位置計算方法:hash & (len -1),原來的len = 2, len - 1 = 1,新的len = 4, len - 1 = 3;用二進制表示為:01 => 11,如果繼續擴容,則(len - 1)的變化規則為:01 => 11 => 111 => 1111 => ...,可以看到,沒擴容一次,hash值的高位就會多一位來參與哈希計算,多一位的這位hash數二進制表現為:要么是0,要么是1,只有這兩種可能,如果為0,則相當于計算出來的哈希槽位置和原來一樣,如果為1,則哈希槽位置會+oldCap,比如對于k1和k2,假設k1的hash計算結果為3,二進制表示為:11,則原來的下標為 (11 & 01) = b01 = 1,擴容后下標計算為:(11 & 11) = b11 = 3,3 = 1 + 2 = oldIndex + oldCap;有了這個知識點,那么就可以繼續來看22這個位置上的代碼了,這里有兩組變量,loHead和loTail是一組,hiHead和hiTail是一組,這兩組分別表示上面分析的兩種情況,也就是loHead和loTail表示那些擴容后依然在原來下標的Node,hiHead和hiTail表示那些擴容后需要移動到oldIdex + oldCap位置上的Node;

  • (23)這里,如果e節點的hash&oldCap==0,說明本次新參與的高位二進制位0,那這個節點擴容后還是在當前的index;

  • (24)這里表示e節點擴容后需要移動到index + oldCap的位置上去;

  • (25、26)到這里,需要將兩條鏈表放到新數組正確的位置上去,這樣就完成了擴容操作

五、 HashMap數據查詢機制


數據查詢比較簡單,我們來分析一下HashMap的get操作是如何完成的;

        public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

上來首先計算了key的hash,然后在調用getNode來實現節點查找,接下來看一下getNode方法的實現;

final Node<K,V> getNode(int hash, Object key) {
        // 1
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        
        // 2
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
            // 3
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            
            // 4
            if ((e = first.next) != null) {
            
                // 5
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                
                // 6
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        
        // 7
        return null;
    }
  • (1)tab變量指向當前table,first是當前哈希槽的第一個節點的引用,e用于迭代鏈表;

  • (2)首先tab賦值,然后需要判斷當前table是否為空,以及first賦值及檢測是否為空,如果這些檢測沒通過,則說明當前HashMap中不可能存在key節點,直接返回null即可;

  • (3)如果first節點就是需要找到的key節點,則直接返回first節點;

  • (4)如果當前哈希槽只有一個節點,那么到此搜索結束,沒有找到key節點,否則,繼續在first結構上查找;

  • (5)如果當前槽內是一顆紅黑樹,則通過紅黑樹的查找方法來查找,這個分支暫時不看;

  • (6)否則,當前槽內就是一條鏈表,那么就需要迭代這條鏈表來找到目標節點;

  • (7)執行到這里,說明HashMap中不存在key節點;

六、 HashMap數據更新機制


數據更新操作主要看一下remove操作是如何實現的:

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

還是會先計算key的哈希值,然后調用removeNode方法來執行刪除操作;下面來看一下removeNode方法的實現細節:

/**
     * Implements Map.remove and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        
        // 1
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        
        // 2
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            
            // 3
            Node<K,V> node = null, e; K k; V v;
            
            // 4
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            
            // 5
            else if ((e = p.next) != null) {
            
                // 6
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                
                // 7
                else {
                    do {
                    
                        // 8
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            
            // 9
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                
                // 10
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                
                // 11
                else if (node == p)
                    tab[index] = node.next;
                
                // 12
                else
                    p.next = node.next;
                ++modCount;
                
                // 13
                --size;
                return node;
            }
        }
        return null;
    }
  • (1)tab是當前table的引用,p指向定位到哈希槽的頭結點,index是當前key節點的哈希槽位置;

  • (2)獲取到p節點,如果p節點為null,說明當前HashMap中不可能存在key,所以刪除失敗,返回null,結束刪除流程;

  • (3)node表示找到的key節點,也就是指向將要被刪除掉的Node;

  • (4)這個分支表示當前槽內的頭結點就是所要刪除的Node,賦值給node變量;

  • (5)表示槽內的頭節點并不是所要刪除的節點,那么就要繼續在p結構中查找,需要判斷一些槽內是否就一個p節點,如果是,那么就可以結束刪除流程,當前HashMap中不可能存在key節點;

  • (6)如果p結構是一顆紅黑樹,那么就要使用查找紅黑樹的方法查找節點;

  • (7)否則,就要在鏈表p中找到key節點;

  • (8)迭代整個p鏈表,找到key節點,并賦值給node變量,但可能沒找到,此時node為null;

  • (9)如果node為null,則說明沒有找到需要刪除的數據,也就是不存在需要刪除的節點,否則,就要執行刪除操作;

  • (10)如果需要刪除的node節點是一個紅黑樹節點,那么就調用紅黑樹的節點刪除方法;

  • (11)如果node和p相等,那么就說明需要刪除的節點是鏈表的頭結點,只需要將頭結點移動到next即可實現節點刪除;

  • (12)否則,就要刪除node節點,此時,p節點就是node節點的前一個節點,刪除node節點只需要執行 p.next = node.next就可以實現;

  • (13)刪除一個節點之后,需要將size減1;

七、 HashMap并發安全問題分析


我們都知道,HashMap是線程不安全的,所謂線程不安全,就是使用不當在并發環境下使用了HashMap,那么就會發生不可預料的問題,一個典型的問題就是cpu 100%問題,這個問題在java 7中是因為擴容的時候鏈表成環造成的,這個成環是因為java 7在遷移節點的時候使用的是頭插法,在java 8中使用了尾插法避免了這個問題,但是java 8中依然存在100%的問題,在java 8中是因為紅黑樹父子節點成環了。
下面,來簡單分析一下java 7中鏈表成環的問題:

 /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                // 1
                Entry<K,V> next = e.next;
                
                // 2
                int i = indexFor(e.hash, newCapacity);
                
                // 3
                e.next = newTable[i];
                
                // 4
                newTable[i] = e;
                
                // 5
                e = next;
            }
        }
    }
image

如圖所示,在擴容前,位置1上有一條長度為3的鏈表,擴容后數組的長度為4,這條鏈表的key=5的節點會被遷移到新數組位置為1的位置上,其余兩個節點會按照尾插法遷移到新數組位置為3的位置上。
假設兩個線程同時指向擴容,thread1執行到代碼位置(1)的時候失去cpu,thread2此時獲得cpu并完成了數據遷移,之后,thread1重新獲得cpu,開始執行遷移操作,此時e執行key=3的節點,next指向key=7的節點,而此時thread2完成遷移后,key=7的節點的next為key=3的節點,此時已經成環,此時如果有線程執行get等查詢操作,那么就可能陷入死循環;如果thread1可以繼續執行遷移,執行注釋中(3)這行代碼后,就將key=3的節點的next指向了key=7的節點,執行(4)后,key=3的節點成了頭結點,執行(5)后,e指向了key=7的節點,接著繼續下一輪遷移;這樣,這個遷移永遠也完成不了,只會不斷在更新槽位的頭結點,死循環了。所以,如果是在并發環境下,我們應該使用線程安全的并發HashMap,

<pre style="margin: 0px; padding: 0px; background-color: rgb(255, 255, 255); color: rgb(0, 0, 0); font-family: "Source Code Pro"; font-size: 12pt;">ConcurrentHashMap是最好的選擇,當然還有其他的方案,但是如果可以使用</pre>

ConcurrentHashMap,其他方案都不推薦。


參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,791評論 6 545
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,795評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,943評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,057評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,773評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,106評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,082評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,282評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,793評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,507評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,741評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,220評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,929評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,325評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,661評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,482評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,702評論 2 380