【Interview】深入理解ConcurrentLinkedQueue原始碼

codeing_docs發表於2019-03-29

概述

  • ConcurrentLinkedQueue是一個基於連結節點的無邊界的執行緒安全佇列,它採用先進先出原則對元素進行排序,插入元素放入佇列尾部,出隊時從佇列頭部返回元素,利用CAS方式實現的
  • ConcurrentLinkedQueue的結構由頭節點和尾節點組成的,都是使用volatile修飾的。每個節點由節點元素item和指向下一個節點的next引用組成,組成一張連結串列結構。
  • ConcurrentLinkedQueue繼承自AbstractQueue類,實現Queue介面

常用方法

  • boolean add(E e) 將指定元素插入此佇列的尾部,當佇列滿時,丟擲異常
  • boolean contains(Object o) 判斷佇列是否包含次元素
  • boolean isEmpty() 判斷佇列是否為空
  • boolean offer(E e) 將元素插入佇列尾部,當佇列滿時返回false
  • E peek() 獲取佇列頭部元素但不刪除
  • E poll() 獲取佇列頭部元素,並刪除
  • boolean remove(Object o) 從佇列中移指定元素
  • int size() 返回此佇列中的元素數量,需要遍歷一遍集合。判斷佇列是否為空時,不推薦此方法

原始碼分析

// 頭節點
 private transient volatile Node<E> head;
 //尾節點
 private transient volatile Node<E> tail;
 
 public ConcurrentLinkedQueue() {
 		//初始化構造時 頭結點等於尾結點
        head = tail = new Node<E>(null);
 }
 //建立一個最初包含給定 collection 元素的 ConcurrentLinkedQueue,按照此 collection 迭代器的遍歷順序來新增元素。
 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;
    }
    
  private static class Node<E> {
    volatile E item;
    volatile Node<E> next;
    //構造一個新節點
   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);
  }
 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);
            }
        }
    }

複製程式碼

offer入隊操作

初始化

  • 初始化操作就是建立一個新結點,並且headtail相等,結點的資料域為空。
    初始化操作
  • 當第一次入隊操作時,檢查插入的值是否為空,為空則拋空指標,然後用當前的值建立一個新Node結點。然後執行死迴圈開始入隊操作
    死迴圈入隊操作
  • 首先定義了兩個指標p和t,都指向tail
  • 然後定義q結點儲存p的next指向的結點,此時p的next是為空沒有結點的
    q指向p的next
  • 此時q==null 條件成立。執行p.casNext(null, newNode).以cas方式把p的下一個節點指向新建立出來的結點,然後往下執行,p=t 直接返回true。此時初始化構造的結點的next指向第一次入隊成功的結點
    第一次入隊成功結構
  • 第二次入隊操作,首先也是非空檢查,然後建立一個新結點。此時死迴圈入隊操作。定義了兩個指標p和t,都指向tail。定義q結點儲存p的next指向的結點,此時p的next是不為空的,指向了上面建立的結點。所以q==null不成立。執行else操作

第二次入隊q==null不成立

  • 此時p也不等於qp!=t不成立,p和t都是指向tail。因為不成立所以讓p=q,此時p和q都是指向第二個結點。再次循迴圈操作。
    image
  • 然後再次p和t,都指向tail。定義q結點儲存p的next指向的結點。此時p的next指向還是空,所以q=null成立。執行p.casNext(null, newNode).以cas方式把pnext指向新建立出來的結點。
    image
  • 此時if (p != t)是成立的 執行casTail(t, newNode); 期望值是t,更新值新建立的結點。於是更新了tail結點移動到最後新增的結點

image
大概的入隊流程就是這樣重複上述操作。直到入隊成功。tail結點並不是每次都是尾結點。所以每次入隊都要通過tail定位尾結點。

    public boolean offer(E e) {
    	//檢查結點是為null,如果插入null則丟擲空指標
        checkNotNull(e);
        //構造一個新結點
        final Node<E> newNode = new Node<E>(e);
        //死循換,一直到入隊成功
        for (Node<E> t = tail, p = t;;) {
        	//p表示佇列尾結點,預設情況尾巴=結點就是taill結點
			//獲取p結點的下一個節點
            Node<E> q = p.next;
            //q為空,說明p就是taill結點
            if (q == null) {
            	//case方式設定p(p=t)節點的next指向當前節點
                if (p.casNext(null, newNode)) {
                    if (p != t) 
                    	//p不等於t更新尾結點,
                        casTail(t, newNode); //失敗了也是沒事的,因為表示有其他執行緒成功更新了tail節點
                    return true;
                }
                //其他執行緒搶先完成入隊,需要重新嘗試
            }
            //q不為空,p和相等
            else if (p == q)
                p = (t != (t = tail)) ? t : head;
            else
            	// // 在兩跳之後檢查尾部更新.
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }
複製程式碼

出隊操作

    public E poll() {
    	//一個標號
        restartFromHead:
        //死迴圈方
        for (;;) {
        	// 定義p,h兩個指標 都指向head
            for (Node<E> h = head, p = h, q;;) {
            	//獲取當前p的值
                E item = p.item;
				//值不為空,且以cas方式設定p的item賦值為空。兩個條件成立向下執行
                if (item != null && p.casItem(item, null)) {
                	// p和h不相等則更新頭結點,否則直接返回值
                    if (p != h) 
                    	//更新頭結點,預期值是h,當p的next指向不為空,更新值是q,為空則是p
                        updateHead(h, ((q = p.next) != null) ? q : p);
                        //返回當前p的值
                    return item;
                }
                //如果item為空說明已經被出隊了,然後判斷q是否null,是空則說明當前佇列為空了。但是q = p.next賦值語句已經執行了
                else if ((q = p.next) == null) {
                //更新頭結點,預期值h=head,更新p.此時p的item是空,說明已經被出隊了
                    updateHead(h, p);
                    return null;
                }
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }
複製程式碼
  • 出隊操作是以死迴圈的方式直到出隊成功。 第一次出隊首先執行for (Node<E> h = head, p = h, q;;) 定義兩個指標ph都指向head
    image
  • 然後定義一個item儲存p(這裡就是head)的值,然後判斷item是否為空,此時第一次出隊時為空的,則執行 else if ((q = p.next) == null) ,此條件不成立,因為head的next有結點。執行 else if (p == q),此時不相等,因為上個操作已經把q賦值為p的next結點了。所以執行最後的else語句 p = q;在次迴圈執行。
    image
  • 此時p.item不為空條件成立且以cas方式更新p的item為空 p.casItem(item, null)。如果都兩個條件都成立,判斷 if (p != h)此時不成立的,更新updateHead(h, ((q = p.next) != null) ? q : p); 預期值是h,更新值是q 因為不為空。並返回item,第一次出隊成功。

第一次出隊成功結構

總結

  • CoucurrentLinkedQueue的結構由頭節點和尾節點組成的,都是使用volatile修飾的。每個節點由節點元素item和指向下一個節點的next引用組成.
  • 入隊:先檢查插入的值是否為空,如果是空則丟擲異常。然後以死循壞的方式執行一直到入隊成功,整個過程大概就是把tail結點的next指向新結點,然後更新tail為新結點即可。但是tail結點並不是每次都是尾結點。所以每次入隊都要通過tail定位尾結點。
  • 出隊:出隊操作就是從佇列裡返回一個最早插入的節點元素,並清空該節點對元素的引用。並不是每次出隊都更新head節點,當head節點有元素時,直接彈出head節點的元素,並以cas方式設定節點的itemnull,不會更新head節點。只有當head節點沒有元素值時,出隊操作才會更新head節點,這種做法是為了減少cas方式更新head節點的消耗,提供出隊的效率

相關文章