JDK source code: ConcurrentLinkedQueue

Keywords: Java JDK

The implementation of blocking queue has been explained before. Today, we will continue to understand the implementation of non blocking queue in the source code. Next, let's see how the non blocking queue of concurrent linked queue completes the operation

Preface

JDK version No.: 1.8.0«

ConcurrentLinkedQueue is an unbounded thread safe FIFO non blocking queue based on linked list. The biggest difference lies in the non blocking feature. The blocking queues explained before will block in various ways. In the ConcurrentLinkedQueue, the non blocking operations will be completed through CAS operations. The update of head and tail is similar to the slack mechanism explained in LinkedTransferQueue. Only when the slack threshold is greater than or equal to 2, can they be updated to minimize the number of CAS operations. Of course, such operations also improve the complexity of code implementation

Class definition

From the diagram, we can see that ConcurrentLinkedQueue does not implement the BlockingQueue interface

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable

Implementation process

In order to understand the operation of its internal implementation, you can see the following process chart to understand the transformation process of its internal nodes entering and leaving the queue

Constant / variable

In addition to the constants that CAS needs to use, there are only two reference nodes: head and tail. In the comment part, you can see the author's description. Here's the explanation:

head node:

  • All undeleted nodes can be accessed by the head node by executing the succ() method
  • head node is not null
  • The head node next cannot point to itself
  • The item of the head node may or may not be null
  • The tail node can lag behind the head node. At this time, the tail node cannot be accessed from the head node

Tail node (tail's next is null):

  • The last node of the queue can be accessed by executing the succ() method through the tail node
  • tail node is not null
  • item of tail node may or may not be null
  • The tail node can lag behind the head node. At this time, the tail node cannot be accessed from the head node
  • The tail node next may or may not point to itself

Because the head node and tail node are not updated in real time, they can only be updated when the relaxation threshold is reached, which may lead to the phenomenon that the head node is behind the tail node

    /**
     * A node from which the first live (non-deleted) node (if any)
     * can be reached in O(1) time.
     * Invariants:
     * - all live nodes are reachable from head via succ()
     * - head != null
     * - (tmp = head).next != tmp || tmp != head
     * Non-invariants:
     * - head.item may or may not be null.
     * - it is permitted for tail to lag behind head, that is, for tail
     *   to not be reachable from head!
     */
    private transient volatile Node<E> head;

    /**
     * A node from which the last node on list (that is, the unique
     * node with node.next == null) can be reached in O(1) time.
     * Invariants:
     * - the last node is always reachable from tail via succ()
     * - tail != null
     * Non-invariants:
     * - tail.item may or may not be null.
     * - it is permitted for tail to lag behind head, that is, for tail
     *   to not be reachable from head!
     * - tail.next may or may not be self-pointing to tail.
     */
    private transient volatile Node<E> tail;
    
    // CAS operation
    private static final sun.misc.Unsafe UNSAFE;
    private static final long headOffset;
    private static final long tailOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = ConcurrentLinkedQueue.class;
            headOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("head"));
            tailOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("tail"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

Inner class

Node implementation is relatively simple, and there is no complex part. It mainly updates variables through CAS operation

    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;

        /**
         * Constructs a new node.  Uses relaxed write because item can
         * only be seen after publication via casNext.
         */
        Node(E item) {
            UNSAFE.putObject(this, itemOffset, item);
        }

        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }

        void lazySetNext(Node<E> val) {
            UNSAFE.putOrderedObject(this, nextOffset, val);
        }

        boolean casNext(Node<E> cmp, Node<E> val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }

        // Unsafe mechanics

        private static final sun.misc.Unsafe UNSAFE;
        private static final long itemOffset;
        private static final long nextOffset;

        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> k = Node.class;
                itemOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

Construction method

The nonparametric construction method creates a null node, and the head and tail nodes point to this null node. It is relatively simple to add nodes in a loop when constructing set parameters. It mainly needs to understand the changes when creating the default nonparametric constructor


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

    public ConcurrentLinkedQueue(Collection<? extends E> c) {
        Node<E> h = null, t = null;
        for (E e : c) {
            checkNotNull(e);
            Node<E> newNode = new Node<E>(e);
            if (h == null)
                h = t = newNode;
            else {
                t.lazySetNext(newNode);
                t = newNode;
            }
        }
        if (h == null)
            h = t = new Node<E>(null);
        head = h;
        tail = t;
    }

Important method

updateHead

Under the premise of H! = p, try to update the head point to p. if it succeeds, try to update the original head node to point to itself, indicating that the node leaves the queue

    /**
     * Tries to CAS head to p. If successful, repoint old head to itself
     * as sentinel for succ(), below.
     */
    final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p))
            h.lazySetNext(h);
    }

succ

Obtain the successor node of the p node. When next points to itself, the node itself may be in the status of leaving the queue. At this time, the head node is returned

    /**
     * Returns the successor of p, or the head node if p.next has been
     * linked to self, which will only be true if traversing with a
     * stale pointer that is now off the list.
     */
    final Node<E> succ(Node<E> p) {
        Node<E> next = p.next;
        return (p == next) ? head : next;
    }

offer

The core method of team entry operation, team entry must be successful, and the return value is true, which means that the team entry will try to operate until it is successful. There are three main situations in the circular attempt:

  • Find the last node and try to update the next node to the new node. If it fails, it means that the next is updated by other threads. At this time, judge again. If it succeeds, judge whether the tail node has lagged more than one node. If it is, try to update the tail
  • The last node found before has left the queue (p = p.next). If the tail has been updated by other threads, it will be updated to the tail. Otherwise, the last node will be found from the head node (because the tail can lag behind the head)
  • This node is not the last node and does not leave the queue at the same time. If the tail has been updated by other threads, it will be updated to the tail. Otherwise, the loop will continue from the next of the current node
    public boolean offer(E e) {
        // Sentence blank
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);

        // Loop until successful
        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            // p this is the last node
            if (q == null) {
                // Start trying to update the next point of p to the newly inserted node
                // If the next of p is not updated successfully, it means that the next has been updated by other nodes first, and the judgment attempt is repeated
                if (p.casNext(null, newNode)) {
                    // Only when more than one node has been added after the tail points to a node can the tail node point be updated
                    // That is to say, update is only attempted when slack > = 2
                    if (p != t) // hop two nodes at a time
                        // Failure may be updated by another thread
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
                // Lost CAS race to another thread; re-read next
            }
            // P is not the last node. At the same time, p = p.next indicates that P has left the team and needs to update P
            else if (p == q)
                // If the tail node has been updated, update the tail. Otherwise, search the last node from the head node
                p = (t != (t = tail)) ? t : head;
            else
                // P is not the last node. At the same time, P is not removed from the queue. If the tail is updated by other threads, P is updated to a new tail. Otherwise, P is updated to p.next to continue the cycle
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

poll

The core method of out of line operation is to try until it is successful. There are four main situations in the cyclic attempt:

  • Find the head node. If the item is not null, try to update it to null to indicate that the node has been queued. If it is successful, judge whether the head node needs to be updated to return the item
  • If the item is null or has been obtained by another thread and the current node is the last node, try to update the header head to point to the current node and return null
  • The current node is not the last node. If it has left the team, it will be cycled from the head again
  • If the current node does not leave the queue, it will be updated to the next node for cycle judgment
    public E poll() {
        restartFromHead:
        // Loop until successful
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;
                
                // If the item is not null, try to update it to null (indicating that the node has been queued)
                if (item != null && p.casItem(item, null)) {
                    // The outgoing node is not the previous head node. If the distance between the old head node h and the actual head node is greater than 1, the head will be updated
                    if (p != h) // hop two nodes at a time
                        // If there is no node after the outbound node, try to update the head node to the outbound node itself (item = null). If there is a node, try to update it to the node after the outbound node
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                // item is empty or has been obtained by another thread
                // If the p node itself is the last node, try to update the head and update the original h node to point to itself, and return null
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                // P is not the last node. If p == p.next, it means that P node has left the team. Then jump to restartFromHead and re cycle the judgment from the beginning
                else if (p == q)
                    continue restartFromHead;
                // If P is not the last node and P does not leave the queue, update p to point to the next node of P, and make a circular judgment
                else
                    p = q;
            }
        }
    }

peek

Similar to the poll method, it does not queue the nodes, but only gets the item value of the header node. Of course, the middle may also help update the head direction

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

first

Similar to the poll method, poll returns an item, and here it returns a node. If it is a null node (item == null), it returns null directly

    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

Delete the element from the queue. Judge whether the node has left the queue by whether the item is null. If yes, continue to judge. If the casItem(item, null) succeeds, it means that the node has been removed successfully. If it fails, it means that it has been dequeued by other threads, then continue to judge

    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;
                // item judgment (non departure node), if not satisfied, continue to judge the successor node
                if (item != null) {
                    if (!o.equals(item)) {
                        next = succ(p);
                        continue;
                    }
                    // If a matching node is found, an attempt is made to update the item to null, indicating that the current node has left the team
                    removed = p.casItem(item, null);
                }
                
                // Subsequent nodes. To this point, the matching nodes have been deleted (deleted by other threads or the current thread)
                next = succ(p);
                // Disassociate matching nodes
                if (pred != null && next != null) // unlink
                    pred.casNext(p, next);
                // If it is a node deleted by the current thread, it will return. Otherwise, it will continue to judge and continue to return until it matches or there is no matching node
                if (removed)
                    return true;
            }
        }
        return false;
    }

addAll

Adding the elements in set c to the queue and to the end of the original queue is similar to the above offer method

    public boolean addAll(Collection<? extends E> c) {
        if (c == this)
            // As historically specified in AbstractQueue#addAll
            throw new IllegalArgumentException();

        // Define two pointer nodes pointing to the head and tail of set c
        // First, transform c into Node list
        Node<E> beginningOfTheEnd = null, last = null;
        for (E e : c) {
            checkNotNull(e);
            Node<E> newNode = new Node<E>(e);
            if (beginningOfTheEnd == null)
                beginningOfTheEnd = last = newNode;
            else {
                last.lazySetNext(newNode);
                last = newNode;
            }
        }
        if (beginningOfTheEnd == null)
            return false;

        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            // p is the last node of the queue
            if (q == null) {
                // Connect the queue with the newly created linked list above, update failure recycling continues
                if (p.casNext(null, beginningOfTheEnd)) {
                    // tail update failed retry
                    if (!casTail(t, last)) {
                        t = tail;
                        if (last.next == null)
                            casTail(t, last);
                    }
                    return true;
                }
            }
            // p is not the last node and has left the team
            else if (p == q)
                // If the tail node has been updated, it will be updated to tail. Otherwise, the last node will be searched from the head node
                p = (t != (t = tail)) ? t : head;
            else
                // P is not the last node. At the same time, P is not removed from the queue. If the tail is updated by other threads, P is updated to a new tail. Otherwise, P is updated to p.next to continue the cycle
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

iterator

The iterator is similar to the iterator explained in the queue before. The source code is not very complex. In the remove method, the item is set to null, and the correlation between the front and back nodes will not operate, so as to prevent problems in multi-threaded traversal

In the construction method, the advance() method is executed, and the node nextNode and its item reference for the next execution are set in advance. hasNext can judge nextNode to ensure the weak consistency of iterators. Once hasNext returns true, the corresponding item will be obtained by calling next, even if the node item has been set to null

    public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {
        /**
         * next Returned Node
         */
        private Node<E> nextNode;

        /**
         * Save the next item to prevent the node from being deleted and calling next to get the value when hasNext is true
         */
        private E nextItem;

        /**
         * If the node returned from the latest call to next is deleted by calling remove, it will be reset to null to avoid deleting the element that should not be deleted
         */
        private Node<E> lastRet;

        /**
         * The Node returned from the first call to next is saved during construction
         */
        Itr() {
            advance();
        }

        /**
         * Moves to next valid node and returns item to return for
         * next(), or null if no such.
         */
        private E advance() {
            lastRet = nextNode;
            E x = nextItem;

            Node<E> pred, p;
            if (nextNode == null) {
                p = first();
                pred = null;
            } else {
                pred = nextNode;
                p = succ(nextNode);
            }

            for (;;) {
                if (p == null) {
                    nextNode = null;
                    nextItem = null;
                    return x;
                }
                E item = p.item;
                if (item != null) {
                    nextNode = p;
                    nextItem = item;
                    return x;
                } else {
                    // Skip null node
                    Node<E> next = succ(p);
                    if (pred != null && next != null)
                        pred.casNext(p, next);
                    p = next;
                }
            }
        }

        public boolean hasNext() {
            return nextNode != null;
        }

        public E next() {
            if (nextNode == null) throw new NoSuchElementException();
            return advance();
        }

        public void remove() {
            Node<E> l = lastRet;
            if (l == null) throw new IllegalStateException();
            // rely on a future traversal to relink.
            l.item = null;
            lastRet = null;
        }
    }

summary

ConcurrentLinkedQueue is an unbounded thread safe FIFO non blocking queue based on linked list. The main part of the overall source code is two points:

  • No lock operation, no block operation, update variables with CAS
  • The head and tail nodes are not updated in real time. Only when slack > = 2 can they be updated

It's easy to clarify its implementation and operation process with the diagram, which is much simpler than the previous LinkedTransferQueue source code

If there is any problem in the above content, please point out that the author will correct it in time after verification. Thank you

Posted by blackhawk08 on Sat, 09 Nov 2019 04:11:35 -0800