Java's juc journey-collection (7)

overview

The juc package also provides n thread-safe collections:

  • ConcurrentHashMap: thread-safe Map
  • ConcurrentLinkedDeque: thread-safe first-in-last-out stack
  • ConcurrentLinkedQueue: thread-safe first-in-first-out queue
  • ConcurrentSkipListMap: thread-safe skip list Map
  • ConcurrentSkipListSet: thread-safe skip list Set
  • CopyOnWriteArrayList: thread-safe snapshot write List
  • CopyOnWriteArraySet: thread-safe snapshot write Set

The following are analyzed one by one.

ConcurrentHashMap

In the JDK1.7 version, the data structure of ConcurrentHashMap is composed of a Segment array and multiple HashEntry, using CAS + ReentrantLock to ensure concurrent security.
In the JDK1.8 version, it is composed of data, single-entry list, and red-black tree, and uses CAS + synchronized to ensure concurrency security.

get

In the JDK1.7 version, the location of the Segment is located by a hash, and then the specified HashEntry is located by the hash, and the linked list under the HashEntry is traversed for comparison. If it succeeds, it will return, and if it fails, it will return null.

In the JDK1.8 version, it can be divided into three steps to describe:

  1. Calculate the hash value, locate the index position of the table, and return if the first node matches
  2. If expansion is encountered, the find method of the ForwardingNode that marks the node being expanded will be called, and the node will be searched, and the match will be returned
  3. If none of the above matches, it will traverse down the node and return if it matches, otherwise it will return null at the end

put

In the JDK1.7 version, when the put operation is performed, the hash of the key will be performed for the first time to locate the position of the segment. If the segment has not been initialized, the value is assigned through the CAS operation, and then the second hash operation is performed to find The position of the corresponding HashEntry, here will use the characteristics of the inherited lock. When inserting data into the specified HashEntry position (the end of the linked list), it will try to acquire the lock by inheriting the tryLock() method of ReentrantLock. If the acquisition is successful, it will Insert directly into the corresponding position. If there is already a thread that acquires the lock of the segment, the current thread will continue to call the tryLock() method to acquire the lock in a spinning manner, and hang up after the specified number of times, waiting for wake-up.

In the JDK1.8 version, it is divided into the following six-step process to outline.

  1. If there is no initialization, call the initTable() method first to perform the initialization process
  2. If there is no hash conflict, insert directly into CAS
  3. If the expansion operation is still in progress, expand the capacity first
  4. If there is a hash conflict, add a lock to ensure thread safety. There are two situations here. One is to traverse directly to the end and insert in the form of a linked list, and the other is to insert a red-black tree according to the red-black tree structure.
  5. In the last one, if the number of the linked list is greater than the threshold value 8, it must first be converted into a black-red tree structure, and break enters the loop again
  6. If the addition is successful, call the addCount() method to count the size, and check whether expansion is required

expansion

In the JDK1.7 version, the size of the Segment[] array is immutable, and the expansion operation is to expand the HashEntry[] in the Segment class to twice the original size.

In the JDK1.8 version, when a key/value node is successfully inserted into the hashMap, it is possible to trigger the expansion action:
1. If the number of elements in the linked list reaches the threshold of 8 after the node is added, the treeifyBin method will be called Convert the linked list into a red-black tree, but before the structure conversion, the length of the array will be judged, as follows:

  • If the array length n is less than the threshold MIN_TREEIFY_CAPACITY, which is 64 by default, the tryPresize method will be called to double the array length, and the transfer method will be triggered to readjust the position of the nodes.

2. After adding a node, the addCount method will be called to record the number of elements and check whether expansion is required. When the number of 64 elements reaches the threshold, the transfer method will be triggered to readjust the position of the node.
3. If the current thread finds that the map is expanding at this time, it will assist in expanding the capacity through the helpTransfer method

ConcurrentLinkedDeque

ConcurrentLinkedDeque is an infinite double-ended queue based on a linked list, thread-safe, and does not allow null elements.
ConcurrentLinkedDeque implements thread synchronization internally through CAS. Generally speaking, if you need to use a thread-safe double-ended queue, then it is recommended to use this class.
Due to the characteristics of the double-ended queue, this class can also be used as a stack, so if you need to use the stack in a concurrent environment, you can also use this class. After JDK1.9, VarHandle was introduced. The access to variables in the JUC package basically uses VarHandle. Take a look at the core members that ensure thread safety:


    private static final VarHandle HEAD;
    private static final VarHandle TAIL;
    private static final VarHandle PREV;
    private static final VarHandle NEXT;
    private static final VarHandle ITEM;

When modifying elements, the CAS of these members is used to ensure thread safety, such as the method that is finally called when adding elements:

private void linkLast(E e) {
    
    
        final Node<E> newNode = newNode(Objects.requireNonNull(e));

        restartFromTail:
        for (;;)
            for (Node<E> t = tail, p = t, q;;) {
    
    
                if ((q = p.next) != null &&
                    (q = (p = q).next) != null)
                    // Check for tail updates every other hop.
                    // If p == q, we are sure to follow tail instead.
                    p = (t != (t = tail)) ? t : q;
                else if (p.prev == p) // NEXT_TERMINATOR
                    continue restartFromTail;
                else {
    
    
                    // p is last node
                    PREV.set(newNode, p); // CAS piggyback
                    if (NEXT.compareAndSet(p, null, newNode)) {
    
    
                        // Successful CAS is the linearization point
                        // for e to become an element of this deque,
                        // and for newNode to become "live".
                        if (p != t) // hop two nodes at a time; failure is OK
                            TAIL.weakCompareAndSet(this, t, newNode);
                        return;
                    }
                    // Lost CAS race to another thread; re-read next
                }
            }
    }

ConcurrentLinkedQueue

It is actually basically the same as the operation mode of ConcurrentLinkedDueue. So only a summary of the features is given:

  1. Important features: linked list structure, non-blocking spin, and unbounded capacity.
  2. Enqueue: Enqueue spins until successful, and inserting null objects is not allowed.
  3. Entry and exit queue order: first in first out.
  4. Non-atomic and non-safe: methods such as the size method and iterators do not guarantee atomicity and safety.
  5. Save CAS overhead: the head and tail node pointers do not necessarily point to the real head and tail nodes.

ConcurrentSkipListMap

A skip list (Skip List) is a data structure similar to a linked list, and the time complexity of its query, insertion, and deletion is O(logn). The characteristics are as follows:

  1. The skip table consists of many layers;
  2. Each layer is an ordered linked list;
  3. The lowest linked list contains all elements;
  4. For any node in each layer, there is not only a pointer to its next node, but also a pointer to its next layer;
  5. If an element appears in the linked list of N level, it will also appear in the linked list below N level.

For example:
As shown in the figure, [1] and [40] nodes have 3 layers, and [8] and [18] nodes have 2 layers. Each level is an ordered linked list. If you want to find the target node [15], the general process is as follows:

  1. First check the first layer of the [1] node, and find that the next node of the [1] node is [40], which is greater than 15, then search for the next layer of the [1] node;
  2. Search the second layer of the [1] node, find that the next node of the [1] node is [8], which is less than 15, then check the next node, and find that the next node is [18], which is greater than 15, so search for [8] the next layer of the node;
  3. Search the second layer of the [8] node, and find that the next node of the [8] node is [10], which is less than 15, then check the next node [13], which is less than 15, then check the next node [15], and find that The value is equal to 15, so the target node is found and the query ends.


insert image description here

ConcurrentSkipListMap uses two structures of Node and Index .
The Node node represents the node that actually stores data, including key, value, and pointer to the next node next:

    static final class Node<K,V> {
    
    
        final K key;     // 键
        V val;           // 值
        Node<K,V> next;  // 指向下一个节点的指针
        Node(K key, V value, Node<K,V> next) {
    
    
            this.key = key;
            this.val = value;
            this.next = next;
        }
    }

The Index node represents the level of the jump table, including the current node node, the next layer down, and the next node right of the current layer:

    static final class Index<K,V> {
    
    
        final Node<K,V> node;   // 当前节点
        final Index<K,V> down;  // 下一层
        Index<K,V> right;       // 当前层的下一个节点
        Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
    
    
            this.node = node;
            this.down = down;
            this.right = right;
        }
    }

As shown in the figure, the Node nodes link the real data in order, and the Index nodes form a multi-level index structure in the jump table.
insert image description here

After talking about the basics, let's talk about why it can guarantee thread safety. Basically, I use spin (for infinite loop) + CAS to ensure that I can modify the value of this node.

ConcurrentSkipListSet

It maintains a ConcurrentSkipListMap and guarantees a unique key. . .

CopyOnWriteArrayList

Core member lock and array:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    
    private static final long serialVersionUID = 8673264195747942595L;

    /**
     * The lock protecting all mutators.  (We have a mild preference
     * for builtin monitors over ReentrantLock when either will do.)
     */
    final transient Object lock = new Object();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
 }

To ensure thread safety is to use synchronized (lock) to lock when modifying elements, and then replace the new array. For example to add elements:

public boolean add(E e) {
    
    
        synchronized (lock) {
    
    
            Object[] es = getArray();
            int len = es.length;
            es = Arrays.copyOf(es, len + 1);
            es[len] = e;
            setArray(es);
            return true;
        }
    }

In this way, it will always be thread-safe when getting. According to this nature, it is used in scenarios with more reads and fewer writes.

CopyOnWriteArraySet

Same as ConcurrentSkipListSet, maintain CopyOnWriteArrayList, and ensure the uniqueness of added elements, such as addIfAbsent.

Guess you like

Origin blog.csdn.net/h295928126/article/details/126046067