头图

Research on the principle of ConcurrentLinkedQueue

introduce

ConcurrentLinkedQueue is thread safe unbounded nonblocking queue , unidirectional bottom list implementation, for the enqueue and dequeue operations achieved using CAS thread safe.

类图

The queue inside the ConcurrentLinkedQueue is implemented using a linked list, in which there are two volatile type Node nodes, 161f7ac22a7fdd, which are used to store the head and tail nodes of the queue, respectively.

As you can see from the no-argument constructor below, the default head and tail nodes point to the sentinel node where item is null. The new element is inserted into the tail of the queue, and the acquired element is dequeued from the head.

private transient volatile Node<E> head;
private transient volatile Node<E> tail;

public ConcurrentLinkedQueue() {
    head = tail = new Node<E>(null);
}

A variable item decorated with volatile is maintained in the Node node to store the value of the node; next stores the next node of the linked list; internally, the CAS algorithm provided by the UNSafe tool class is used to ensure the atomicity of the linked list operation when entering and leaving the queue.

offer action

The offer operation is to add an element to the end of the queue. If the passed parameter is null, an exception will be thrown. Otherwise, since the ConcurrentLinkedQueue is an unbounded queue, the method will always return true. In addition, because the CAS algorithm is used, this method will not be used. Blocking suspends the caller thread. Let's look at the source code:

public boolean offer(E e) {
        //(1)为null则抛出异常
        checkNotNull(e);
       //(2)构造Node节点,内部调用unsafe.putObject
        final Node<E> newNode = new Node<E>(e);
        //(3)从尾节点插入
        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            //(4)如果q == null则说明p是尾节点,执行插入
            if (q == null) {
                //(5)使用CAS设置p节点的next节点
                if (p.casNext(null, newNode)) {
                    //(6)如果p != t,则将入队节点设置成tail节点,更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
            }
            else if (p == q)
                //(7)多线程操作时,由于poll操作移除元素后可能会把head变成自引用,也就是head的next变成了head,这里需要重新找新的head
                p = (t != (t = tail)) ? t : head;
            else
                //(8)寻找尾节点
                p = (p != t && t != (t = tail)) ? t : q;
        }
}

Let's analyze the execution flow of this method. First, when the method is called, code (1) checks the passed parameters, throws an exception if it is null, otherwise executes code (2) and uses item as the constructor parameter to create a new one node, and then the code (3) starts to loop from the tail node of the queue, intending to add elements from the tail, and the queue state is as shown in the following figure when the code (4) is reached.

队列状态

At this time, the nodes p, t, head, and tail all point to the sentinel node whose item is null. Since the next node of the sentinel node is null, q is also null.

Code (4) If q == null, execute code (5), use CAS to judge whether the next node of p node is null, if it is null, use newNode to replace the next node of p, and then execute code (6), but this time p = = t so no tail node is set, then exit the offer method. The current queue status is as follows:

队列状态

What we just explained is the case where one thread calls the offer method, but if multiple threads call at the same time, there will be a situation where multiple threads execute code (5) at the same time.

Suppose thread 1 calls offer(item1), thread 2 calls offer(item2), and both execute code (5), p.casNext(null, newNode) at the same time. Since CAS is atomic, we assume that thread 1 executes the CAS operation first and finds that p.next is null, and then updates it to item1. At this time, thread 2 will also judge whether p.next is null, and jump if it finds that it is not null. to code (3), then execute code (4). At this time, the queue distribution is as follows:

队列分布

Then thread 2 finds that code (4) is not satisfied, it jumps to execute code (8), and then assigns q to p. The queue status is as follows:

队列状态

Then thread 2 jumps to code (3) again for execution. When code (4) is executed, the queue state is as follows:

队列状态

At this time, q == null , so thread 2 will execute code (5), and judge whether the p.next node is null through CAS operation. If it is null, use item2 to replace it. If not, continue to loop. Assuming that CAS is successful, execute code (6). Since p != t , set the tail node to item2, and then exit the offer method. At this time, the queue distribution is as follows:

队列状态

We haven't talked about the code (7) until now. In fact, this step is mainly executed after the poll operation is performed. The following things may happen when executing poll:

队列状态

Let's analyze that if it is in this state, call offer to add elements. When executing code (4), the state diagram is as follows:

队列状态

Here, since the q node is not empty and p == q is executed, the code (7) is executed because t == tail so p is assigned as head, and then the loop is re-executed, and the code (4) is executed after the loop. The queue status is as follows:

队列状态

At this time, because of q == null , the code (5) is executed to perform the CAS operation. If there is no other thread currently performing the offer operation, the CAS operation will be successful, and the p.next node is set as a new node. Then execute the code (6). Because of p != t new node is set as the tail node of the queue. Now the queue status is as follows:

队列状态

📢 Note that self-referencing nodes here will be garbage collected

summarizes : The key step of the offer operation is code (5). The CAS operation is used to control that only one thread can append elements to the end of the queue at the same time, and the failed thread will try the CAS operation in a loop until the CAS succeeds.

add operation

The add operation is to add an element at the end of the linked list, and the offer method is called internally.

public boolean add(E e) {
    return offer(e);
}

poll operation

The poll operation gets and removes an element at the head of the queue, or returns null if the queue is empty. Let's take a look at the implementation principle of this method.

public E poll() {
    //(1)goto标记
    restartFromHead:
    //(2)无限循环
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            //(3)保存当前节点
            E item = p.item;
                         //(4)当前节点有值切CAS变为null
            if (item != null && p.casItem(item, null)) {
                //(5)CAS成功后标记当前节点并移除
                if (p != h) // hop two nodes at a time
                    //更新头节点
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
           //(6)当前队列为空则返回null
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            //(7)如果当前节点被自引用,则重新寻找头节点
            else if (p == q)
                continue restartFromHead;
            else  //(8)如果下一个元素不为空,则将头节点的下一个节点设置成头节点
                p = q;
        }
    }
}
final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
        h.lazySetNext(h);
}

Since the poll method is to obtain an element from the head and remove it, the inner loop of code (2) starts from the head node, and code (3) obtains the head node of the current queue. When the queue is empty at the beginning, the queue status is as follows:

队列状态

Since the item pointed to by the head node is a null sentinel node, the code (6) will be executed. Assuming that no thread calls the offer method during this process, then q is equal to null at this time, and the queue status is as follows:

队列状态

Then execute the updateHead method, because h == p , the head node is not set, and then returns null.

Assuming that when code (6) is executed, another thread has called the offer method and successfully added an element to the queue. At this time, q points to the newly added element node, as shown in the following figure:

队列状态

Then code (6) (q = p.next) == null not satisfied. When code (7) p != q executes code (8), and then points p to node q. The queue status is as follows:

队列状态

Then the program executes code (3) again, p is not pointing to null now, then execute p.casItem(item, null) to try to set the item value of p to null through CAS operation, if CAS succeeds at this time, execute code (5), at this time p != h is set The head node is p, and the next node of h is set to itself, poll then returns the removed node item. The queue diagram is as follows:

队列状态

The current queue state is the state of code (7) when we just talked about the offer operation.

Now we still have code (7) that has not been executed, let's see when it is executed. Assume that when thread 1 performs the poll operation, the queue status is as follows:

队列状态

Then execute p.casItem(item, null) to try to set the item value of p to null through the CAS operation. If the CAS setting is successful, mark the node and remove it from the queue. The queue status is as follows:

队列状态

Then, because of p != h , the updateHead method will be executed. If thread 1 executes updateHead before another thread 2 starts the poll operation, at this time, the p of thread 2 points to the head node, but the code (6) has not been executed yet. At this time, the queue status is as follows :

队列状态

Then thread 1 executes the updateHead operation. After the execution, thread 1 exits. At this time, the queue status is as follows:

队列状态

Then thread 2 continues to execute code (6), q = p.next , because the node is a self-referential node, so p == q , so it will execute code (7) and jump to the outer loop restartFromHead to get the current queue head head, and the current status is as follows:

队列状态

Summary : When the poll method removes an element, it simply uses the CAS operation to set the item value of the current node to null, and then removes the element from the queue by resetting the head node, and the removed node is Become an orphan node, this node will be recycled during garbage collection. In addition, if the head node is found to be modified in the execution branch, jump to the outer loop to obtain the new head node again.

peek operation

The peek operation is to get an element at the head of the queue (only get but not remove), and return null if the queue is empty. Let's take a look at its implementation principle.

public E peek() {
   //1.
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            //2.
            E item = p.item;
            //3.
            if (item != null || (q = p.next) == null) {
                updateHead(h, p);
                return item;
            }
            //4.
            else if (p == q)
                continue restartFromHead;
            //5.
            else
                p = q;
        }
    }
}

The code structure of the peek operation is similar to that of the poll operation, except that the castItem operation is missing from code (3). In fact, this is normal, because peek only gets the value of the queue head element, and does not clear its value. According to the previous introduction, we know that after the first execution of offer, the head points to the sentinel node (that is, the node whose item is null), then when peek is executed for the first time item == null will be found in code (3), and then q = p.next will be executed. When the q node points to the first real element in the queue, or if the queue is null, q points to null.

When the queue is empty, the queue looks like this:

队列状态

At this time, updateHead is executed. Since the h node is equal to the p node, no operation is performed, and then the peek operation will return null.

When there is at least one element in the queue (assuming there is only one), the queue state is as follows:

队列状态

At this time, code (5) is executed, p points to the q node, and then code (3) is executed. At this time, the queue status is as follows:

队列状态

When executing code (3), it is found that item is not null, so the updateHead method is executed. Because of h != p , the head node is set, and the queue status after setting is as follows:

队列状态

Finally, the sentinel node is removed.

Summary : The code of the peek operation is similar to the poll operation, except that the former only gets the head element of the queue but does not delete it from the queue, while the latter needs to delete it from the queue after getting it. In addition, when calls the peek operation for the first time, it will delete the sentinel node and make the head node of the queue point to the first element in the queue or null .

size operation

Calculating the number of elements in the current queue is not very useful in a concurrent environment, because CAS is not locked, so it is possible to add or delete elements from calling the size method to returning the result, resulting in inaccurate counted elements.

public int size() {
    int count = 0;
    for (Node<E> p = first(); p != null; p = succ(p))
        if (p.item != null)
            // Collection.size() spec says to max out
            if (++count == Integer.MAX_VALUE)
                break;
    return count;
}
//获取第一个队列元素,(剔除哨兵节点),没有则为null
Node<E> first() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            boolean hasItem = (p.item != null);
            if (hasItem || (q = p.next) == null) {
                updateHead(h, p);
                return hasItem ? p : null;
            }
            else if (p == q)
                continue restartFromHead;
            else
                p = q;
        }
    }
}

remove operation

If the element exists in the queue, delete the element, if there are more than one, delete the first one, and return true, otherwise return false.

public boolean remove(Object o) {
    if (o != null) {
        Node<E> next, pred = null;
        for (Node<E> p = first(); p != null; pred = p, p = next) {
            boolean removed = false;
            E item = p.item;
            if (item != null) {
              // 若不匹配,则获取next节点继续匹配
                if (!o.equals(item)) {
                    next = succ(p);
                    continue;
                }
              //相等则使用CAS设置为null
                removed = p.casItem(item, null);
            }
            // 获取删除节点的后继节点
            next = succ(p);
            //如果有前驱节点,并且next节点不为null,则链接前驱节点到next节点
            if (pred != null && next != null) // unlink
              // 将被删除的节点通过CAS移除队列
                pred.casNext(p, next);
            if (removed)
                return true;
        }
    }
    return false;
}

contains operation

Judging whether the queue contains the specified object, because the entire queue is traversed, the result like the size operation is not so accurate. It is possible that the element is still in the queue when this method is called, but the element is deleted by other threads during the traversal process. Then it will return false.

public boolean contains(Object o) {
    if (o == null) return false;
    for (Node<E> p = first(); p != null; p = succ(p)) {
        E item = p.item;
        if (item != null && o.equals(item))
            return true;
    }
    return false;
}

Summarize

The bottom layer of ConcurrentLinkedQueue uses a singly linked list data structure to store queue elements, and each element is packaged as a Node node. The queue is maintained by the head and tail nodes. When the queue is created, the head and tail nodes point to a sentinel node whose item is null. The first peek or first operation will point head to the first real queue element. Since the non-blocking CAS algorithm is used, there is no lock, so offer, poll or remove operations may be performed when calculating the size, resulting in inaccurate number of calculated elements, so the size method is not very useful in concurrent situations.


神秘杰克
765 声望383 粉丝

Be a good developer.