Source code analysis of ConcurrentHashMap 3.put() method

Posted by PHPThorsten on Thu, 25 Nov 2021 21:48:57 +0100

1.putVal() method

General process of writing data

Pre write operation

1. ConcurrentHashMap does not allow NULL key or value, and an exception will be thrown

2. Before writing data, the hash value of the key will be processed once. spread()

Write data flow

The entire write data is a spin (dead loop) operation.

  • Case 1: the current table has not been initialized. Call initTable() to try to initialize the table and continue spinning
  • Case 2: the current table has been initialized. The element on the bucket obtained by calling the addressing algorithm (hash & (table. Length - 1)) is NULL. Try to write the constructed node to the bucket using CAS operation. Successfully exit the cycle, fail to continue spinning.
  • Case 3: the current table has been initialized, but the node of the current bucket is an FWD node, which indicates that the table is being expanded, and the current thread will participate in the expansion. Continue to spin after capacity expansion.
  • Case 4: explain that the current bucket position forms a linked list or red black tree. First lock the current bucket position with synchronized locks (the lock object is the head node of the current bucket position), and then judge that if the current bucket position forms a linked list, traverse the nodes in the linked list to find out whether there are nodes that are completely consistent with the keys of the nodes to be inserted. If there are, replace them with values, Insert the node to the end of the linked list without. If the current bucket has formed a red black tree, find out whether there is a node whose key is exactly the same as the node to be inserted in the tree, and replace the value if it exists. If it doesn't exist, insert it into the tree.

Post write operation

  • ① If the current bucket location is a linked list, it will check whether it meets the tree standard and then de tree it.
  • ② If the node to be inserted conflicts with a node on the bucket bit and is replaced (whether it is a linked list or a red black tree), directly return the value of the conflicting node without executing the logic of ③ (because this is a replacement operation and no new node is added)
  • ③ If there is no conflict, it means that this is an addition operation. You need to call the addCount() method. Internally, count the nodes in the table, and then judge whether they meet the expansion standard and execute the expansion logic
   public V put(K key, V value) {
        //putVal method called.
        return putVal(key, value, false);
    }

     /*
      *  @param key key of element
      *  @param value Element value
      *  @param onlyIfAbsent Replace data
      *    false --> When put ting data, if there is the same k-v data in the Map, replace it
      *    true  --> If there is the same k-v data in the Map, it will not be replaced or inserted
      */
     final V putVal(K key, V value, boolean onlyIfAbsent) {
        /*   Note that this is different from HashMap,
         *   HashMap The key or value is allowed to be null. The ConcurrentHashMap does not allow the key or value to be null		 
         *   value Is null.
         */
        if (key == null || value == null) throw new NullPointerException();
        
        //Recalculate the hash value through the spread method (refer to the source code analysis 2 spread method for details)
        int hash = spread(key.hashCode());
        
        /*
         * binCount Indicates the subscript position of the linked list in the bucket after the current k-v is encapsulated into node and inserted into the specified bucket
         * 0 The current bucket bit is null, and node can be placed directly
         * 2 Indicates that the current bucket may be a red black tree
         */
        int binCount = 0;
 
        /*
         * spin
         * tab Reference hash table
         */ 
        for (Node<K,V>[] tab = table;;) {
            
            /*
             *  f Represents the head node of the bucket
             *  n Indicates the length of the hash table
             *  i Indicates the bucket coordinates obtained by the key through addressing calculation  
             *  fh Hash value of bucket head node
             */
            Node<K,V> f; int n, i, fh;
  		    
            /*
             *  CASE1 : Indicates that the table has not been initialized
             *  
             *   Enter the initTable() method and try to initialize the table. Finally, the initialization completed table is returned.
             */
            if (tab == null || (n = tab.length) == 0)
                tab = initTable(); 
             
            /*
             *  CASE2 : table It has been initialized, and it is found that the current bucket head node is null after addressing
             *  Addressing algorithm: ((table. Length - 1) & hash) is consistent with HashMap addressing algorithm
             *  
             *   Enter casTabAt and try to add a Node using CAS.
             */
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //The i-th position of the tab is expected to be NULL. If it is NULL, the created Node will be assigned
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    //Set successfully, end the spin. Failed (another thread succeeded in writing) take other logic to continue spinning
                    break;                  
            }
            
            /*
             * CASE3: table It has been initialized, and after addressing, it is found that the current bucket head node is FWD node, indicating the current bucket head node			   
             * map It is in the process of capacity expansion (migration). 
             *   (MOVED == -1 Indicates that the current node is an FWD node)
             *   
             *   Entering helpTransfer is obliged to help the current map object complete the data migration.
             */
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            
            
            /*
             * CASE4: The current bucket (not NULL) may be a linked list or a red black tree proxy class TreeBin 
             */
            else {
                //oldVal returns the old value when the key of the inserted node exists.
                V oldVal = null;
                
                /*
                 * synchronized Lock the head node of the current bucket (theoretically the head node). (only for the current bucket position)				 
                 *  Lock, do not lock the whole Map)
                 */
                synchronized (f) {
                    
                    /* 
                     *  Why do you want to compare here to see if the first few points of the current bucket position are the previously obtained heads					 
                     * 	Node?
                     *   (tabAt()Is to get the node at i location)
                     *  In order to avoid that other threads have changed the head node of the bucket, resulting in the wrong lock of the current thread,					 
                     * 	There will be problems. If the comparison fails here, it does not need to be executed.
                     *
                     *  If the conditions are established, you can come in and make it!
                     */
                    if (tabAt(tab, i) == f) {
                        //If the condition is true, it indicates that the current bucket position forms a linked list.
                        if (fh >= 0) {
                            
                            /*
                             * 1,
                             * The current inserted key is inconsistent with the keys of all elements in the linked list							 
                             *  The operation is to append the node to the end of the linked list, and binCount represents the length of the linked list.
                             * 2,
                             *  When the current insert key is consistent with the key of an element in the linked list, the current insert operation							  
                             *  As a replacement, binCount represents the conflict location (binCount-1)   
                             */
                            binCount = 1;
                            
                            /*
                             * Traverse the linked list to determine whether the key of a node is completely the same as the key of the current element to be inserted							  
                             * Consistent, replace value if it exists, and insert it at the end of the linked list if it does not exist.
                             *
                             */
                            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;
                                //Insert to end
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //The current bucket level has changed to red black tree
                        else if (f instanceof TreeBin) {
                            //p means that if there is a conflict with the key of the node you inserted in the red black tree, the conflicting node will be assigned to p
                            Node<K,V> p;
                            
                            //binCount is forced to be set to 2 because binCount < = 1 has other meanings,
                            binCount = 2;
                            
                            //p is not null, indicating that there is a conflict.
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value; //Overwrite the value of the original key
                            }
                        }
                    }
                }
                //This indicates that the current bucket is not null. It may be a red black tree or a linked list
                if (binCount != 0) {
                    //Bincount > = 8 indicates that the current bucket location must be a linked list
                    if (binCount >= TREEIFY_THRESHOLD)
                        //Possible tree (depending on the length of the table)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        //Returns the value of the conflicting node.
                        return oldVal;
                    break;
                }
            }
        }
        /*
         *  After exiting the spin, call addCount.
         * 1,Count the total number of nodes in the current table
         * 2,Judge whether it is enough to meet the expansion standard. 
         */
        addCount(1L, binCount);
        return null;
    }

2.initTable() method

initTable method flow.

The current thread attempts to initialize the table. There are two situations

  • The value of sizeCtl is - 1, which means that another thread is initializing the table. The current thread releases the CPU execution right first, and then tries again until the table is initialized and exits the loop.
  • The value of sizecl is greater than or equal to 0. The current thread attempts to initialize the table using CAS. After initializing the table * * (only one thread can actually initialize the table), change the sizeCtl value to 0.75 of the current table length, that is, the capacity expansion threshold * *.

Source code analysis

    private final Node<K,V>[] initTable() {
        
        /*
         *  tab Represents a reference to a table                          
         *  sc Represents the value of the temporary sizeCtl
         */
        Node<K,V>[] tab; int sc;
        
        
        /*
         * Spin with condition (the current table has not been initialized). Exit after the table is initialized.
         */
        while ((tab = table) == null || tab.length == 0) {
            
            /*
             *  First determine the value of sizeCtl,
             *  1,sizeCtl < 0 
             *     1.1 sizeCtl = -1 Indicates that a thread is creating a table array, and the current thread needs to wait
             *     1.2 Others indicate that the current table array is being expanded. The high 16 bits indicate the expansion timestamp, and the low 16 bits indicate that (1 + nThread) threads are being expanded concurrently
             *  2,sizeCtl = 0
             *    Indicates that default is used when creating a table array_ Capability (16) size
             *
             *  3,sizeCtl > 0 
             *     Two cases   
             *     1.If the table is not initialized, it indicates the initialization size
             *     2.If the table has been initialized, it indicates the trigger condition (threshold) for the next capacity expansion  
             */
            if ((sc = sizeCtl) < 0)
                //The value of sizeCtl is probably - 1, that is, another thread is currently creating a table array
                //yield means to release CPU execution rights.
                Thread.yield(); // lost initialization race; just spin
            
            
             /*
             *  There are two situations that will come here
             *  2,sizeCtl = 0
             *    Indicates that default is used when creating a table array_ Capability (16) size
             *
             *  3,sizeCtl > 0 
             *     Two cases   
             *     1.If the table is not initialized, it indicates the initialization size
             *     2.If the table has been initialized, it indicates the trigger condition (threshold) for the next capacity expansion      
             *  
             *     Obtain the lock by calling the CAS operation of UnSafe to modify the value of sizeCtl
             *     Note: the sizectl constant of CAS method here is the memory address of sizectl variable, that is, the value of sizectl variable is modified.
             */
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    //Still judge whether the table is null to prevent multiple initialization under multithreading.
                    if ((tab = table) == null || tab.length == 0) {
                        /*
                         * sc If it is greater than 0, sc is used to specify the size when creating the table,
                         * Otherwise, use the default value of 16 to initialize the table
                         */
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        //Create Node []
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        //Assign to table
                        table = tab = nt;
                        // sc becomes 0.75n, that is, the expansion threshold.
                        sc = n - (n >>> 2);
                    }
                } finally {
                    /*
                     * There are two situations
                     * 1,The current thread is the thread that initializes the table for the first time, and the sc at this time is the capacity expansion threshold
                     * 2,The current thread is not the thread that initializes the table for the first time, so it will not enter the initialization table
                     * But the value of sizeCtl has been changed to - 1. Finally, you need to change its value back
                     */
                    sizeCtl = sc;
                }
                break;
            } 
        }
        //Finally, return to tab.
        return tab;
    }

3.addCount() method

Execution logic of addCount() method

1. Call the add logic similar to LongAdder first, and try to write data first,

If the cells array has been initialized or CAS fails to write data to the base, it will continue to judge the condition of the cells array, and then enter fullAddCount (that is, the longAccumulate() method in LongAdder).

If the write is successful, the sum of the number of nodes will be calculated once.

Note: if a thread really calls longAccumulate(), return it directly. It neither sums nor participates in the logic of table array expansion

2. Enter the logic of capacity expansion, first calculate a capacity expansion stamp, and then spin the logic to judge whether the current thread can participate in capacity expansion * * (see source code analysis for four conditions) * *. If it can participate, use CAS to modify the value of sizeCtl (at this time, sizeCtl is a negative number, the high 16 bits represent the timestamp, and the low 16 bits represent the number of threads participating in capacity expansion). CAS attempts to add the capacity expansion thread + 1, If successful, enter the expansion logic, and if failed, continue to spin.

 private final void addCount(long x, int check) {
     
     	/*
     	 * This part of the code is the code in LongAdder, first add(), then call sum() to seek the sum.
     	 * as Represents an array of cells in LongAdder
     	 * b Represents the base in LongAdder 
     	 * s Indicates the number of nodes in the table
     	 */
        CounterCell[] as; long b, s;
      	
     	/*
     	 * Two conditions (or relationship) for entering if
     	 * Condition 1: the cells array has been initialized
     	 * Condition 2: the cells array is not initialized, but CAS fails to write to the base (competition occurs),
     	 */   
        if ((as = counterCells) != null ||
           !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            
            /*
             * a Indicates the cell hit after addressing by the current thread
             * v Indicates the expected value when the current thread writes the cell
             * m Represents the length of the current cells array
             */
            CounterCell a; long v; int m;
            
            //true indicates that there is no competition, and false indicates that there is competition
            boolean uncontended = true;
            
            /*
             * fullAddCount()The method is the longAccumulate() method in LongAdder.
             *  For the detailed longAccumulate() method, refer to the LongAdder source code analysis
             *  There are three situations where you enter the longAccumulate() method
             *  1,cells Array is NULL
             *  2,cells The array is not NULL, but the Cell hit after addressing by the current thread is NULL.
             *  3,cells The array is not NULL, and the hit Cell is not NULL, but try CAS to write to the Cell				
             *  Data failure
             */
            
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                //Detailed explanation and reference[ https://blog.csdn.net/qq_46312987/article/details/121499330 ]
                fullAddCount(x, uncontended);
                
                /*Considering that fullAddCount() is very tiring, let the current thread not participate in the logic of capacity expansion.
                Straight back.*/
                return;
            }
            if (check <= 1)
                return;
            //Summation (the sum method in LongAdder) is an expected value (final consistency)
            s = sumCount();
        }
//-------------------------------LongAdder Code ENd --------------------------    
//----------------------------------Expansion logic Start----------------------------- 	 
        /*
         *  check >=0 Indicates that it must be an addCount called by a put operation
     	 */
        if (check >= 0) {
            /*
             *  tab Reference table
             *  nt Indicates nexttable (expanded table)
             *  n  Represents the length of the table array
             *  sc Represents a temporary value for sizeCtl
             */
            Node<K,V>[] tab, nt; int n, sc;
            
             /*
             *  First judge the value of sizeCtl, and then come to the possible situation of sizeCtl
             *  1,sizeCtl < 0 
             *   (Impossible) 1.1 sizeCtl = -1 indicates that a thread is creating a table array, and the current thread needs to wait
             *          1.2 Others indicate that the current table array is being expanded. The high 16 bits represent the time stamp of expansion, and the low 16 bits represent the time stamp of expansion 
             *          (1 + nThread)Threads are expanding concurrently (possible)	
             *
             *  2,sizeCtl = 0
             *    Indicates that default is used when creating a table array_ Capability (16) size (impossible)
             *
             *  3,sizeCtl > 0 
             *     Two cases   
             *     1.If the table is not initialized, it indicates the initialization size (impossible)
             *     2.If the table has been initialized, it indicates the trigger condition (threshold) for the next capacity expansion (possible)
             */
            
            /*
             *  spin
             *  Condition 1: s > = (long) (SC = sizectl)
             * 		    true  -> 1.The current sizeCtl is a negative number, indicating that capacity expansion is in progress	
             *			false -> 2.The current sizeCtl is a positive number, indicating the capacity expansion threshold, and the current element				
             *	        The number of elements does not reach the expansion threshold, so there is no need to expand
             *
             *  Condition 2: (tab = table)= Null constant established
             *  
             *  Condition 3: (n = tab. Length) < maximum_ CAPACITY)
             *         true Indicates that the current table length is less than the maximum limit and can be expanded
             */
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                
                /*
                 *  Unique indication stamp of expansion batch
                 *  Assume that RS calculated by current 16 - > 32 = 1000 000 0001 1011
                 */
                int rs = resizeStamp(n);
                
                /*
                 *   sc Less than 0 indicates that the current table is expanding
                 *   The CAS attempt of the current thread should assist table in capacity expansion
                 */
                if (sc < 0) {
                    
                    /*
                     * Whether the current thread can be expanded.
                     * 
                     * Condition 1: 
                     *  (sc >>> RESIZE_STAMP_SHIFT) != rs 
                     *   true Indicates the capacity expansion obtained by the current thread. The unique stamp is not the capacity expansion of this batch 
                     *   false Indicates the capacity expansion obtained by the current thread. The only indication stamp is the capacity expansion of this batch
                     *  
                     * Condition 2:
                     *   sc == rs + 1 (There is a BUG here, and the subsequent JDK versions have been modified)
                     *   In fact, what I want to express is SC = = RS < < 16 + 1
                     *   true Indicates that the capacity expansion is completed and the current thread does not need to participate
                     *   false Indicates that the capacity expansion is still in progress and the current thread can participate
                     *
                     *  Condition 3:
                     *    sc == rs + MAX_RESIZERS (There is a BUG here, and the subsequent JDK versions have been modified)
                     *    The actual expression is SC = = RS < < 16 + max_ RESIZERS
                     *   true Indicates that the current number of threads participating in capacity expansion has reached the maximum number, and the current thread does not need to participate
                     *   false Indicates that the number of currently participating expansion threads has not reached the maximum number, and the current thread can participate
                     *   
                     *   Condition 4:
                     *     (nt = nextTable) == null 
                     *    true Indicates the end of this capacity expansion (nextTable is set to NULL only when it is not NULL during capacity expansion)  
                     *    false Indicates that this expansion is not over  
                     */
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    
                    /*
                     * Precondition: the current table is being expanded, and the current thread has the opportunity to participate in the expansion
                     * Because the current table is expanding, it means that the value of sizeCtl must be negative, which is represented by the lower 16 bits
                     * This is the number of threads currently participating in capacity expansion, so try + 1 to enter the logic of capacity expansion.
                     *  
                     *  Attempt failed:
                     *  1.At present, many threads are trying to modify sizeCtl here, and another thread is modified to
                     *  The result is that your sc expected value is inconsistent with the value in memory, and the modification fails
                     *  
                     *  2.transfer The thread inside the task also modified sizeCtl.
                     *  Failure continues to spin.
                     */
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        //Assist in expanding the thread and hold the nextTable parameter
                        transfer(tab, nt);
                }
                
                /*
                 *  CAS Modify the value of sizeCtl. If successful, change sizeCtl to a negative number, indicating that the current scene is triggered
                 *  For the first thread of capacity expansion, some extra work needs to be done in the transfer() method
                 */
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    //The thread that triggers the expansion condition does not hold the nextTable.
                    transfer(tab, null); 
                //Sum.
                s = sumCount();
            }
        }
    }

4. Summary

  • (1) The storage method of the number of elements is similar to the LongAdder class, which is stored in different segments to reduce the conflict when different threads update the size at the same time;

  • (2) When calculating the number of elements, add the values of these segments and baseCount to calculate the total number of elements;

  • (3) Under normal conditions, sizeCtl stores the expansion threshold, which is 0.75 times the capacity;

  • (4) During capacity expansion, sizectl (sizectl < 0) stores the capacity expansion stamp in high order, and the number of capacity expansion threads in low order is increased by 1 (1+nThreads)

  • (5) After adding elements to other threads, if it is found that there is capacity expansion, it will also try to join the capacity expansion column of the;

Topics: Java Back-end JUC