Deep Analysis ConcurrentLinkedQueue Principle

Posted by toppac on Sun, 05 May 2019 18:08:04 +0200

title: Deep analysis ConcurrentLinkedQueue principle
date: 2019-04-23 20:19:04
categories:

  • Java Concurrency
    tags:
  • Concurrent Container

Introduction

There are many thread-safe concurrent containers in the JUC package that can be used to achieve thread safety without having to manually set locks.Among these concurrent containers, there are blocking and non-blocking (simply, threads can be blocked when they place and remove elements).ConcurrentLinkedQueue is implemented in a non-blocking manner. This concurrent container class implements a thread-safe queue by cycling CAS operations. It does not cause the current thread to be paused and therefore avoids the overhead of thread context switching.

Blocking containers are thread-safe through locks, whereas non-blocking is achieved through CAS.

Principle of CAS

CAS(Compare and Swap): Compare and exchange.It is the name for a processor instruction that CAS implements in Java by calling JNI code.The CAS operation, as its name implies, compares before exchanging (or updating), as seen by a pseudocode:

for(;;){
    ...
     public boolean cas(V a, V b ,V c){
        //A is the variable you want to modify, b is the value of a when the current thread calls the CAS operation (that is, the old value of a), and c is the new value

        if(b == a){ //Check if other threads have modified a
            b = c; //To update
            return true; //Update Successful
        }

        return false; //update operation
    }  
    ...
}

When a thread is performing a CAS operation, it can modify it if the value it wants to modify is the same as the old value provided by the thread calling the CAS operation, indicating that other threads have not modified it.The other threads fail to update and continue trying until they succeed.

ConcurrentLinkedQueue principle

Simply put, ConcurrentLinkedQueue is equivalent to the thread-safe version of LinkedList, which is a one-way chain list with a private static class Node <E> inside it, which encapsulates elements and saves the next node (next), where Node <E> performs CAS operations through a UNSAFE class inside.

The main code for the Node class:

private static class Node<E> {
        volatile E item; // volatile declaration
        volatile Node<E> next; // volatile declaration
        // Construction method at address itemOffset, value replaced by item
        Node(E item) {
            UNSAFE.putObject(this, itemOffset, item);
        }
        // CAS operation: Compare and exchange atomic variables that change the itemOffset address.If the value of the variable is cmp and is successfully replaced with valReturns true  
        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }
       // Replace the value of the nextOffset address with x
        void lazySetNext(Node<E> val) {
            UNSAFE.putOrderedObject(this, nextOffset, val);
        }
        // CAS operation: Compare and exchange atomic variables that change the nextOffset address.If the value of the variable is cmp and is successfully replaced with valReturns true
        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 code block, get itemOffset and 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);
            }
        }
    }

CAS operations for tail and head nodes:

  	// CAS operation: Compare and exchange atomic variables that change tailOffset address.If the value of the variable is cmp and is successfully replaced with valReturns true
	private boolean casTail(Node<E> cmp, Node<E> val) {
        return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val);
    }
 	// CAS operation: Compare and exchange atomic variables that change the headOffset address.If the value of the variable is cmp and is successfully replaced with valReturns true
    private boolean casHead(Node<E> cmp, Node<E> val) {
        return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val);
    }

UNSAFE class methods used:

 // Gets the offset address of f in heap memory
 public native long objectFieldOffset(Field f);

 // Gets the offset address of static f in heap memory
 public native long staticFieldOffset(Field f);

 // Atomic variable that changes offset address.If the value of the variable is expected and is successfully replaced with x to return the true CAS operation
 public final native boolean compareAndSwapObject(Object o, long offset, Object expected,Object x);

 // Replace the value of the offset address with x and notify other threads.Because there is a Volatile, similar to putObject
 public native void putObjectVolatile(Object o, long offset, Object x);

 // Get a value with offset address
 public native Object getObjectVolatile(Object o, long offset)

Two variables tail and head are maintained in ConcurrentLinkedQueue, and an empty node is created and assigned to tail and head when the ConcurrentLinkedQueue instance is created.

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

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

Queue (add)

Queuing means adding the newly created ode Node to the end of the queue, as follows ( Pictures from www.ifeve.com).

  • First, add the element 1 node after the head node, that is, the next node of the head node is element 1, because the tail node and the head node are the same node when the ConcurrentLinkedQueue is initialized.
  • In the second step, set the element 2 node as the next node of the element 1 node and the tail node as the element 2 node.
  • ~~
public boolean offer(E e) {
        checkNotNull(e);
    	//Create a new queued node
        final Node<E> newNode = new Node<E>(e);
		//Create a reference t pointing to the tail node, which is equivalent to an intermediate variable used to compare with p
    	//p means tail node
    	//Dead loop, keep trying CAS operation, value until success, return true
        for (Node<E> t = tail, p = t;;) {
         	//Get the next node of the p node
            Node<E> q = p.next;
            //If q is empty, p is the tail node
            if (q == null) {
                //p is the tail node, after adding a new node to the p node, CAS operation: compare and exchange, if the value of nextOffset equals null, indicating that other threads have not modified it, then the CAS operation succeeds and jumps out of the loop
                if (p.casNext(null, newNode)) {
                    //If p is not equal to t, tail is not the tail node, then the new node is set as the tail node by the CAS operation, and if it fails, another thread has modified it
                    if (p != t) 
                        //If tailOffset has a value of t, try setting the new node to the tail node
                        casTail(t, newNode);  
                    return true;
                }
                
            }
            //If p equals q, p and Q are both empty, that is, ConcurrentLinkedQueue has just been initialized
            else if (p == q)
            
                p = (t != (t = tail)) ? t : head;
            //p has a next node, which means that the next node of p is the tail node, then you need to update p and point it at the next node
            else
                
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

  • Get the tail node first.

  • The next node of the tail node is then determined to be empty.

    • If empty, try adding nodes using CAS operations

      • Returns true if successful.
      • If it fails, it means that another thread has added a new node, then the tail node needs to be retrieved.
    • If it is not empty, it indicates that a new node has been added by another thread.

      • Just initialized.
      • Update the tail node.

Through analysis, it is known that each time a queue is enrolled, the tail node will be located first. After successful positioning, the queue will be enrolled by p.casNext(null, newNode) CAS operation, because P and T are not the same every time, that is, tail node will not be reset every time a queue is enrolled, which reduces the number of casTail(t, newNode) CAS operations to set the tail node, reduces overhead, and improves entryTeam efficiency.

Out of Queue (poll)

Out of the queue is to eject the head node from the queue, empty its reference, and return the elements in the node.

As with queuing, the head node is not updated every time an out-of-queue operation occurs. Updating the head node only occurs when the element of the head node is empty.

    public E poll() {
        //Dead loop, queue out operation, until successful, return, either an item or null.
        restartFromHead:
        for (;;) {
           	//Create a reference h pointing to the header node, which is equivalent to an intermediate variable used to compare with p
    		//p represents the header node
            for (Node<E> h = head, p = h, q;;) {
                //item is the element of the header node
                E item = p.item;
				//If the p-node element is not empty, the CAS operation is used: If the value of the item address of the current thread equals the value of the itemOffset address, set the element value of the p-node to empty and return the item
                if (item != null && p.casItem(item, null)) {
                  	//If p is not equal to h, that is, the head is not the head node, set the next node of p as the head node
                    if (p != h) 
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                //Indicates that it is the last node and jumps out of the loop
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                //p is the same as q, Q and p are empty, skip to outer loop, start over
                else if (p == q)
                    continue restartFromHead;
                //P has a next node, set p to point to its next node
                else
                    p = q;
            }
        }
    }
  • First get the element of the header node.
  • Then determine if the header node element is empty.
    • If empty, it means another thread has queued to remove elements from the node.
    • If not null, use CAS to set the reference of the header node to null.
      • If CAS succeeds, the elements of the header node are returned directly.
      • If unsuccessful, it means that another thread has updated the head node with a queue operation, causing the element to change and requiring the head node to be retrieved.

Topics: Java