Java 8 ConcurrentHashMap source code analysis

1. The underlying structure of ConcurrentHashMap

The underlying data structure of ConcurrentHashMap in JDK 1.8 is basically the same as HashMap. The two are almost identical in terms of capacity mechanism, Entry hash value calculation and array index subscript positioning. Readers who are interested in this section can refer to it a>Java 8 HashMap detailed explanation

2. Element storage procedure of ConcurrentHashMap

ConcurrentHashMap#putVal()The method source code is as follows, from which we can see that its processing has the following key steps:

  1. ConcurrentHashMap does not support null keys and null values
  2. Call the ConcurrentHashMap#spread() method to recalculate the hash of the current key,The core is high and low 16-bit XOR to increase the degree of discreteness.
  3. Assign the value of volatile variable table to temporary storage as tab, and use for loop to process element storage.
    1. First determine whether the underlying array has been initialized. If not, call the ConcurrentHashMap#initTable() method to initialize the array first
    2. Determine an array subscript based on the hash operation of key, call the ConcurrentHashMap#tabAt() method to obtain the first element on the array subscript, if the element is null, there is no conflict, directly CallConcurrentHashMap#casTabAt() method CAS to insert element
    3. If the hash value of the first element of the array index is MOVED(-1), it means that this element is ForwardingNode node, the existence of this node indicates that ConcurrentHashMap is expanding, so elements cannot be inserted in this cycle. Call the ConcurrentHashMap#helpTransfer() method to help expand, Elements cannot be inserted until the expansion is completed
    4. If there is a hash conflict between the first element of the array subscript and the current key, use synchronized to lock the element. If the element is a linked list node, directly encapsulate the new element into the node and insert it into the end of the linked list. ; If the element is an encapsulation object of a tree node, it means that the linked list at this subscript position has been converted into a red-black tree, and the insertion method of the red-black tree can be called. After the element is inserted, if it is checked that the total number of linked list nodes at the current array subscript has reached 8, you need to call the ConcurrentHashMap#treeifyBin() method to try to convert it into a red-black tree
  4. After the element is stored, call the ConcurrentHashMap#addCount() method to update the element counter. If expansion check is required, verify whether the current total number of elements is greater than the capacity control threshold sizCtl, and if so, perform expansion
    final V putVal(K key, V value, boolean onlyIfAbsent) {
    
    
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
    
    
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    
    
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
    
    
                V oldVal = null;
                synchronized (f) {
    
    
                    if (tabAt(tab, i) == f) {
    
    
                        if (fh >= 0) {
    
    
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
    
    
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
    
    
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
    
    
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
    
    
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
    
    
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
    
    
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
  1. ConcurrentHashMap#initTable()The method logic is relatively simple, and the important processing is as follows:

    1. If the underlying array table has not been initialized, enter the while loop
    2. According to the capacity control thresholdsizeCtl Determine whether other threads are currently initializing. If the value is -1, it means that other threads are initializing, and the current thread gives up CPU resources. So that the initialization operation can be completed as quickly as possible
    3. If the call Unsafe#compareAndSwapInt() to update sizeCtl is -1 successfully, the current thread performs the creation of the underlying array, and sizeCtl are reset to 0.75 times the length of the underlying array, used as the expansion threshold
    private final Node<K,V>[] initTable() {
          
          
         Node<K,V>[] tab; int sc;
         while ((tab = table) == null || tab.length == 0) {
          
          
             if ((sc = sizeCtl) < 0)
                 Thread.yield(); // lost initialization race; just spin
             else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
          
          
                 try {
          
          
                     if ((tab = table) == null || tab.length == 0) {
          
          
                         int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                         @SuppressWarnings("unchecked")
                         Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                         table = tab = nt;
                         sc = n - (n >>> 2);
                     }
                 } finally {
          
          
                     sizeCtl = sc;
                 }
                 break;
             }
         }
         return tab;
     }
    
  2. ConcurrentHashMap#treeifyBin()The logic of the method is relatively simple. First, determine whether the length of the underlying array is less than 64. If so, call the ConcurrentHashMap#tryPresize() method to try to expand the capacity by 2 times; secondly, if the first element of the current array subscript is still Linked list node, then lock the node, convert the current linked list into a tree node linked list, and finally Complete the red-black tree construction in the TreeBin construction method

        private final void treeifyBin(Node<K,V>[] tab, int index) {
          
          
         Node<K,V> b; int n, sc;
         if (tab != null) {
          
          
             if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                 tryPresize(n << 1);
             else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
          
          
                 synchronized (b) {
          
          
                     if (tabAt(tab, index) == b) {
          
          
                         TreeNode<K,V> hd = null, tl = null;
                         for (Node<K,V> e = b; e != null; e = e.next) {
          
          
                             TreeNode<K,V> p =
                                 new TreeNode<K,V>(e.hash, e.key, e.val,
                                                   null, null);
                             if ((p.prev = tl) == null)
                                 hd = p;
                             else
                                 tl.next = p;
                             tl = p;
                         }
                         setTabAt(tab, index, new TreeBin<K,V>(hd));
                     }
                 }
             }
         }
     }
    
     private final void tryPresize(int size) {
          
          
         int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
             tableSizeFor(size + (size >>> 1) + 1);
         int sc;
         while ((sc = sizeCtl) >= 0) {
          
          
             Node<K,V>[] tab = table; int n;
             if (tab == null || (n = tab.length) == 0) {
          
          
                 n = (sc > c) ? sc : c;
                 if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
          
          
                     try {
          
          
                         if (table == tab) {
          
          
                             @SuppressWarnings("unchecked")
                             Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                             table = nt;
                             sc = n - (n >>> 2);
                         }
                     } finally {
          
          
                         sizeCtl = sc;
                     }
                 }
             }
             else if (c <= sc || n >= MAXIMUM_CAPACITY)
                 break;
             else if (tab == table) {
          
          
                 int rs = resizeStamp(n);
                 if (sc < 0) {
          
          
                     Node<K,V>[] nt;
                     if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                         sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                         transferIndex <= 0)
                         break;
                     if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                         transfer(tab, nt);
                 }
                 else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                              (rs << RESIZE_STAMP_SHIFT) + 2))
                     transfer(tab, null);
             }
         }
     }
    
    
  3. ConcurrentHashMap#addCount()The method first updates the element counter through Unsafe. Secondly, if the input parameter specifies that expansion needs to be checked, it determines whether the total number of elements is greater than the sizeCtl capacity threshold. If so, it needs to enter the expansion operation. The key logic here is as follows:

    1. When the expansion operation has not yet started, sizeCtl still stores the capacity threshold, which is a positive number at this time. The current thread uses Unsafe to update it to a negative number calculated by a specific algorithm. After, then call ConcurrentHashMap#transfer() to start the expansion operation
    2. WhensizeCtl is a negative number, it means that the expansion operation has started. At this time, the current thread uses Unsafe to add 1 to it as the expansion thread count, and then calls ConcurrentHashMap#transfer() Pass in the cache array of the expansion operationnextTable for subsequent expansion operations
     private final void addCount(long x, int check) {
          
          
         CounterCell[] as; long b, s;
         if ((as = counterCells) != null ||
             !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
          
          
             CounterCell a; long v; int m;
             boolean uncontended = true;
             if (as == null || (m = as.length - 1) < 0 ||
                 (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                 !(uncontended =
                   U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
          
          
                 fullAddCount(x, uncontended);
                 return;
             }
             if (check <= 1)
                 return;
             s = sumCount();
         }
         if (check >= 0) {
          
          
             Node<K,V>[] tab, nt; int n, sc;
             while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                    (n = tab.length) < MAXIMUM_CAPACITY) {
          
          
                 int rs = resizeStamp(n);
                 if (sc < 0) {
          
          
                     if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                         sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                         transferIndex <= 0)
                         break;
                     if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                         transfer(tab, nt);
                 }
                 else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                              (rs << RESIZE_STAMP_SHIFT) + 2))
                     transfer(tab, null);
                 s = sumCount();
             }
         }
     }
    

3. Expansion of ConcurrentHashMap

3.1 The process of expansion

From the source code point of view, there are two main expansion opportunities for ConcurrentHashMap:

  1. The total number of elements of ConcurrentHashMap is greater than the capacity control thresholdsizeCtl
  2. The number of linked list elements at an array subscript reaches the tree threshold 8, but the length of the current underlying array is less than 64

The entire capacity expansion process from triggering to processing is as follows, and the key steps are supplemented as follows:

  1. When thread A adds elements to ConcurrentHashMap and checks that the total number of elements in the current Map reaches the expansion threshold, the expansion operation is triggered.
  2. When expanding, each thread is only responsible for migrating a part of the subscripted elements in each round. The migration progress is controlled by the offset transferIndex, which is initialized to the length of the underlying array
  3. According to the number of CPU cores and the length of the underlying array, the number of migration array subscripts in each round is allocated, with a minimum of 16.After determining the span value, combined with transferIndex, the array subscript range allocated to the current thread for this round of migration can be determined, and updated after the allocation is completedtransferIndex , and then start the migration from the end of the array
  4. During the migration process of thread A, a Forwarding node will be left as a mark on each migrated subscript of the original array. When other threads recognize the marked node, they need to perform corresponding processing.For example, when thread B adds an element and finds that the element on the array subscript is a Forwarding node, it knows that it is currently being expanded and needs help with expansion; when thread B obtains an element, it finds a Forwarding node on the subscript. , it means that the elements at the subscript have been migrated to the new array. At this time, you need to call ForwardingNode#find() to search and obtain the elements in the new array
  5. The expansion process is controlled by sizeCtl. Each time an additional thread joins the expansion, the value will be increased by 1. If no other thread joins the expansion, a single thread will continue to copy and move elements to the new array in the migration method until transferIndex is 0
  6. When transferIndex is 0, there are no elements in the array to be migrated, and each thread will reset sizeCtl when it exits the expansion. When the value of sizeCtl is reset to the original negative number, it means that this thread is the last expansion thread. Then the operation after the expansion is completed and the original array is rechecked from the end to see if there are any missing unmigrated elements. , after completion, use the migrated nextTable to replace the original array, and reassign the sizeCtl value to the expansion threshold of the new array

Insert image description here

3.2 Source code analysis

There are many trigger points for expansion, but no matter where it is triggered, the ConcurrentHashMap#transfer() method is ultimately called. It should be noted that before each thread is added to the expansion, it will be counted on through U.compareAndSwapInt(this, SIZECTL, sc, sc + 1) with the help of the Unsafe class, sizeCtlThe main role of this variable during expansion is to ensure that the last thread that exits the expansion completes the migration omission check and array replacement.. ConcurrentHashMap#transfer() The source code of the method is as follows. You can see that the processing is roughly divided into the following parts:

  1. Determine the number of migration array subscripts in each round based on the number of CPU cores, the minimum is 16
  2. If the new array nextTab has not been created yet, create a new array twice the length of the original array and initialize it transferIndex The offset is the length of the original array
  3. The code for migration processing in the for loop is roughly divided into the following parts:
    1. First, in the while loop, the determination of the array subscript range for this round of migration is completed. After that, after each array subscript is migrated, the right boundary i will be moved to the left until the offset of transferIndex is 0, assign the right boundary i to -1
    2. Calculate whether the expansion of the current thread has ended based on the value of the right boundary i. In this part, the count is first reset by U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1) on sizeCtl . Determine whether the current thread is the last thread to exit the expansion based on the calculation result of this variable. If so, update the flag bitfinishing, assign the right boundary i to the length of the original array, and recheck from the end of the original array. Are there any omissions in the migration? When the check is completed, replace the original array with the new array according to the flag finishing to complete the expansion
    3. The right boundary i moves from right to left. If there is no element on the original array subscript i during the traversal process, you only need to insert a Forwarding node as a mark at this position. If the element at this position is already a Forwarding node, set advance to true and skip processing. If there are elements that need to be migrated on the array subscript i, first lock the head node with synchronized, and perform corresponding migration processing according to the different data structures. After the migration is completed, leave a Forwarding node as a mark at the original array subscript position. . The algorithm for determining the subscript of the element in the new array can refer to HashMap expansion mechanism
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    
    
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {
    
                // initiating
            try {
    
    
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {
    
          // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
    
    
            Node<K,V> f; int fh;
            while (advance) {
    
    
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
    
    
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
    
    
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
    
    
                int sc;
                if (finishing) {
    
    
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
    
    
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
    
    
                synchronized (f) {
    
    
                    if (tabAt(tab, i) == f) {
    
    
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
    
    
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
    
    
                                int b = p.hash & n;
                                if (b != runBit) {
    
    
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
    
    
                                ln = lastRun;
                                hn = null;
                            }
                            else {
    
    
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
    
    
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
    
    
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
    
    
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
    
    
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
    
    
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

Guess you like

Origin blog.csdn.net/weixin_45505313/article/details/130647442