Deadly Java concurrent programming (9): Unbounded thread-safe queue ConcurrentLinkedQueue source code analysis

This article is not difficult to understand. Compared with ConcurrentHashMap, it is simpler, because it does not involve operations such as expansion and data migration, I believe you will definitely gain after reading it.

This article is Sike Java Concurrency series of nine, the protagonist is java and contract provided ConcurrentLinkedQueueit is a thread-safe and unbounded queue (because it is based on a linked list to achieve, so unbounded), often needed in concurrent programming When it comes to thread-safe queues, when interviewing thread pools, the queues in them can also be implemented using this queue. It is thread-safe and uses a first-in, first-out order.

Through the study of the entire series of concurrency articles, we can think of two ways to implement a thread-safe queue: one is to use the blocking method , that is , the form of lock , add synchronized or obtain lock in the dequeue and enqueue methods Realize by locking etc. The other is a non-blocking method , which is achieved using spin CAS. In the Java concurrency package, you will see that the classes at the beginning of Concurrent support concurrency, which is non-blocking.

In this article, let’s analyze how the concurrency master Doug Lea uses a non-blocking method to implement the thread-safe queue ConcurrentLinkedQueue from the source code analysis. I believe we can learn a lot of concurrency skills from the master.

ConcurrentLinkedQueue structure

Analyze its structure through the class diagram of ConcurrentLinkedQueue:

Insert picture description here

It can be seen that ConcurrentLinkedQueue is composed of head nodes and tail nodes, and each node, which is a subclass of Node, is composed of node attributes item and next (reference to the next node Node). That is, the nodes are related by next to form a linked list.

Enqueuing means packing elements into nodes and putting them at the end of the linked list each time, and dequeuing deletes an element from the head of the linked list and returns.

After understanding the overall structure, you should also see that the most important thing about the ConcurrentLinkedQueue is the two operations, enqueue and dequeue. Next, we will learn directly from the source code level.

Enqueue operation

The process of joining the team is actually adding the joining node to the end of the queue. In fact, Master Doug Lea has made some optimizations for this joining operation. In order to see the source code more clearly, here is a set of process diagrams of joining the team for an intuitive understanding. Some optimization points. Suppose you want to insert four nodes now:

Insert picture description here

Through the above operations of entering the team, we observed the changes of the head and tail nodes. The conclusion is actually two things: The first is to set the enqueue node to the next node of the current tail node; the second is to update the tail node. If the next node of the tail node is not empty, set the enqueue node as the tail node; if the next node of the tail node is empty, set the enqueue node as the next node of the tail node . That is to say, the tail node is not necessarily the end node. It must be clearly remembered. This is very useful for understanding the following source code of the queue.

I won't say much below, just look at the code directly, understand the above description, and combine the code comments, I believe you will be able to understand:


// 将指定的元素插入到此队列的末尾,因为队列是无界的,所以这个方法永远不会返回 false 。
public boolean offer(E e) {
    
    
    checkNotNull(e);
    // 入队前,创建一个入队节点
    final Node<E> newNode = new Node<E>(e);
    // 死循环,入队不成功反复入队
    // 创建一个指向tail节点的引用,p用来表示队列的尾节点,默认情况下等于tail节点
    for (Node<E> t = tail, p = t;;) {
    
    
        // 获得p节点的下一个节点
        Node<E> q = p.next;
        // next节点为空,说明p是尾节点
        if (q == null) {
    
    
            // p is last node
            // p是尾结点,则设置p节点的next节点为入队节点
            if (p.casNext(null, newNode)) {
    
    
                // 首先要知道入队操作不是每次都设置tail节点为尾结点,为了减少CAS操作提高性能,也就是说tail节点不总是尾节点
                // 如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
                // p != t 这个条件者结合下面 else 分支看,下面在冲突的时候会修改 p 指向 p.next,所以导致p不等于 tail,
                // 即tail节点有大于等于1个的next节点
                if (p != t) // hop two nodes at a time
                    // 如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
                    // 这里允许失败,更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        // 这个分支要想进去,即 p 节点等于 p.next 节点,只有一种可能,就是 p 节点和 p.next 节点都为空
        // 表示这队列刚初始化,正准备添加节点,所以需要返回head
        else if (p == q)
            // 如果tail变了,说明被其他线程添加成功了,则 p 取新的 tail,否则 p 从 head 开始
            p = (t != (t = tail)) ? t : head;
        else
            // Check for tail updates after two hops.
            // 进行这个分支说明next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点
            // 执行 p != t 说明尾结点不等于tail,t != (t = tail)) 说明tail做了变动,
            // 同时满足说明tail已经重新设置了,即结尾就是tail,否则尾结点取tail.next
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

From the source code point of view, the entire enqueue process mainly does two things: the first is to locate the tail node; the second is to use the CAS algorithm to set the enqueue node as the next node of the tail node, and try again if it fails.

Let's think about it here. The above analysis of the entry and exit operation is to set it as the end node in the first-in-first-out queue. Master Doug Lea's code is a bit complicated. Can we replace it with the following code?

public boolean offer(E e) {
    
    
    if (e == null) 
        throw new NullPointerException();
    Node<E> n = new Node<E>(e); 
    for (;;) {
    
    
        Node<E> t = tail;
        if (t.casNext(null, n) && casTail(t, n)) {
    
     
            return true; 
        } 
    } 
}

The above code sets tail as the end node every time it enters the team, which saves a lot of code and is easier to understand.
However, the disadvantage of this is that the loop CAS is used every time to set the tail node. If the CAS can be reduced to update tail nodes, the efficiency of enqueue can be improved. But we also have to consider that since tail is not necessarily equal to the tail node, one more loop operation is needed when entering the queue to locate the end node.

But this efficiency is still high, because it is volatile variable node tail, nature point of view is to pass through increases to reduce the read-write volatile variables volatile variables , and for overhead volatile writing is far greater than the read operation, so the team Efficiency will increase. Great God’s pursuit of performance is real to the extreme, the source code is still useful to read! !

Dequeue operation

Speaking of dequeuing operations, you will definitely think that dequeuing should reduce the update head node, and directly pop up the head of the queue to reduce CAS update operations to improve performance?

With this question in mind, let's look down together. In order to facilitate readers' understanding, here is a set of snapshots of dequeue operations.

Insert picture description here

It can be seen from the figure that the head node is not updated every time you leave the team. If there are elements in the head node, the element in the head will be directly popped up, and the reference to the element by the node will be cleared. If the element in the head node is empty, the head node will be updated.

With a general understanding, you can read the source code to analyze:

// 从队头出队
public E poll() {
    
    
    restartFromHead:
    for (;;) {
    
    
        // p 表示头节点,需要出队的节点
        for (Node<E> h = head, p = h, q;;) {
    
    
            // 获取p节点的元素
            E item = p.item;
            // 如果头节点p中元素不为空,则进行CAS清空p节点对元素的应用,返回p节点的元素
            if (item != null && p.casItem(item, null)) {
    
    
                // CAS 设为成功后进入到这里,需要判断头节点p是否和head节点不是同一个了,即头节点p已经变更了
                if (p != h) // hop two nodes at a time
                    // 更新head节点,将p节点的next节点设为head节点,如果p.next不为空则设置p.next,否则设置p本身
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            // 到这说明头节点p中的元素为null或者发生冲突CAS失败,CAS失败也说明被别的线程取走了当前元素,所以就该取一个节点即next节点
            // 如果队列头节点p的next节点为空,说明队列已空,将则head设为p,返回null
            else if ((q = p.next) == null) {
    
    
                updateHead(h, p);
                return null;
            }
            // 进到这里说明 (q = p.next) == null 返回false,即p的next不为空
            // 同时q=p.next,而这个分支是说p=p.next,只有队列初始化时满足条件,两者都为空,则返回head,重头开始赋值
            else if (p == q)
                continue restartFromHead;
            else
                // 说明p节点的next不为空,且队列不是初始化状态,所以头节点p指向p.next
                p = q;
        }
    }
}

First get the element of the head node, and then judge whether the element of the head node is empty. If it is empty, it means that another thread has performed a dequeue operation to remove the element of the node. If it is not empty, use CAS to remove The reference of the head node is set to null. If the CAS is successful, the element of the head node is directly returned. If it is unsuccessful, it means that another thread has performed a dequeue operation and updated the head node, causing the element to change and the head needs to be retrieved again. node.

In the dequeue operation, when the head node is not equal to the head node p, the head will be set to the latest head node of the team only when the head node is not equal to the head node p, which reduces CAS operations and improves efficiency.

to sum up

  1. ConcurrentLinkedQueue is unbounded because the structure is composed of linked lists, which is inherently unbounded, and of course it is limited by the size of system resources;
  2. ConcurrentLinkedQueue adopts the operation of reducing the head and tail of CAS update when entering and leaving the queue, which improves the performance;
  3. ConcurrentLinkedQueue is implemented in a non-blocking mode, that is, no lock, thread safety is achieved through spin and CAS;

The thread-safe unbounded queue ConcurrentLinkedQueue source code in the concurrent package I learned today is not difficult. I believe it will give you a deeper understanding and facilitate future use in work and interviews.

The author's level is limited, and there will inevitably be errors in the article. If there are errors, welcome to discuss and discuss them. I will correct them as soon as possible. I have seen it all here, the code word is not easy, cute, you remember to "like" Oh, I need your positive feedback.

(End of the full text) fighting!

Personal public account

Insert picture description here

  • Friends who feel that they are writing well can bother to like and follow ;
  • If the article is incorrect, please point it out, thank you very much for reading;
  • I recommend everyone to pay attention to my official account, and I will regularly push original dry goods articles for you, and pull you into the high-quality learning community;
  • github address: github.com/coderluojust/qige_blogs

Guess you like

Origin blog.csdn.net/taurus_7c/article/details/106075750