Implementation Principle of Concurrent LinkedQueue in jdk1.8

Summary

Concurrent LinkedQueue is an unbounded thread-safe queue based on link nodes, which uses FIFO rules to sort nodes. When we add an element, it will be added to the end of the queue; when we get an element, it will return to the element at the head of the queue. A non-blocking thread-safe queue is implemented with CAS.
The implementation of Concurrent LinkedQueue's non-blocking algorithm can be summarized as follows:

1. Using CAS atomic instructions to process concurrent access to data is the basis of non-blocking algorithm.
2. head/tail does not always point to the head/tail node of the queue, that is to say, the queue is allowed to be inconsistent. This feature separates the two steps that need to be atomized together when entering or leaving the team, thus narrowing the range of atomized update values to the only variable when entering or leaving the team. This is the key to the realization of non-blocking algorithm.
3. Update head/tail in batch mode to reduce the overhead of entry/exit operations as a whole.

In the source code of Concurrent LinkedQueue, there are some basic invariant conditions specified by a scarlet letter.

1. The next field in the last node of the queue is null.

2. The item fields of all undeleted nodes in the queue cannot be null and can be traversed from head in O(N) time.

3. For a node to be deleted, instead of setting its reference directly to null, the item field is set to null (iterator skips a node whose item is null in traversal).

4. Allow head and tail lag updates, that is, the head/tail mentioned above does not always point to the head/tail node of the queue (this is mainly to reduce the number of CAS instructions executed, but at the same time increase the number of volatile reads, but this consumption is small). Specifically, when an element is inserted into the queue, it detects whether the distance between tail and the last node is at or above two nodes (internally known as hop); when leaving the queue, the head is detected to be at two distances from the first node of the queue, or the head is pointed to the first node and the next domain of the node originally pointed to by the head is pointed to itself. This allows disconnection from the queue to help GC

Invariance and variability conditions of head

Invariance:

1. All undeleted nodes can be traversed from head by calling succ() method.

2. head cannot be null.

3. The next field of the head node cannot be referenced to itself.

Variability:

1. The item field of the head node may or may not be null.

2. Allow tail lag behind the head, that is to say, traversing the queue from head does not necessarily lead to tail.

Invariance and variability conditions of tail

Invariance:

1. The succ() method is called through tail, and the final node is always reachable.

2. tail cannot be null.

Variability:

1. The item domain of tail node may or may not be null.

2. Allow tail to lag behind head, that is to say, traversing queues from head does not necessarily lead to tail.

3. The next domain of tail node can be referenced to itself.

structure

Concurrent LinkedQueue is composed of head node and tail node. Each node has a node element item and a reference to the next node. It is through this next connection between nodes and nodes that a queue of linked list structure is formed. By default, the head node stores empty elements and the tail node equals the head node.

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

Node nodes are composed as follows:

    static final class Node<E> {
        volatile Node<E> prev;
        volatile E item;
        volatile Node<E> next;

        Node() {  // default constructor for NEXT_TERMINATOR, PREV_TERMINATOR
        }

        /**
         * Construct a new node. Use UNSAFE.putObject, because item can only be seen after it is published through casNext or casPrev
         */
        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);
        }

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

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

        // Unsafe mechanics

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

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

Queue entry

1. Entry process


Add element 1: The next node of the queue update head node is element 1. Because tail nodes are equal to head nodes by default, their next nodes all point to element 1 nodes.
Add element 2: The queue first sets the next node of element 1 node to element 2, and then updates tail node to point to element 2.
Add element 3: Set the next node of tail node to element 3 node
Add element 4: Set the next node of element 3 to element 4, and then point the tail node to element 4.

From the above description, we can see that there are two main things to do in the queuing process: first, to set the queuing node as the next node of the last node in the current queue; second, to update the tail node. If the next node of the tail node is not empty, the new added node will be set as the tail node; if the next node of the tail node is empty, the new added node will be set as the next node of the tail node. Node.

In concurrent case, if a thread is queuing, it must first get the tail node, and then set the next node of the tail node as the queuing node, but there may be another thread queuing, then the tail node of the queue will send a change, which is that the current thread will pause the queuing operation, and then retrieve the tail node.

private void linkLast(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);

        restartFromTail:
        //Dead cycle, try again and again until you succeed in entering the team
        for (;;)
            //T denotes tail node, p denotes the last node of the queue, which by default equals t
            for (Node<E> t = tail, p = t, q;;) {
            //The next node of the tail node is not empty, and the next node of the last node of the queue is not empty, indicating that other threads completed the entry first.
                if ((q = p.next) != null &&
                    (q = (p = q).next) != null)
                    //Then q is the node that other threads join the queue. Set p to the last node and point to Q. T!= (t = tail) indicates that the queuing node of another thread is queued, but the queuing node
                    p = (t != (t = tail)) ? t : q;
                else if (p.prev == p) // NEXT_TERMINATOR
                    continue restartFromTail;
                else {
                    // p is the last node of the queue
                    newNode.lazySetPrev(p); // CAS operation, set the precursor node of the queuing node to p
                   if (p.casNext(null, newNode)) {//CAS operation, set the next node of last node as the queue node

                        if (p != t) //The last node of the queue is not equal to the tail node
                            casTail(t, newNode);  // The CAS operation sets the incoming node to tail node
                        return;
                    }

                }
            }
    }

Posted by Stu on Sat, 30 Mar 2019 12:00:30 -0700