非阻塞佇列ConcurrentLinkedQueue與CAS演算法應用分析

天啦擼發表於2018-12-06

ConcurrentLinkedQueue是無阻塞佇列的一種實現, 依賴與CAS演算法實現。

入隊offer

  1. if(q==null)當前是尾節點 -> CAS賦值tail.next = newNode, 成功就跳出迴圈
  2. elseif(p == q)尾節點被移除 -> 從tail或head重新往後找
  3. else不是尾節點 -> 往next找

規則定義:

當一個節點的next指向自身時, 表示節點已經被移除, 註釋中還會強調這一點。

完整程式碼(JDK8):

/**
 * Inserts the specified element at the tail of this queue.
 * As the queue is unbounded, this method will never return {@code false}.
 *
 * @return {@code true} (as specified by {@link Queue#offer})
 * @throws NullPointerException if the specified element is null
 */
/*
* 變數說明:
*   成員變數: 
*       head: 首節點
*       tail: 尾節點, 不一定指向末尾, 兩次入隊才更新一次
*   區域性變數
*         t= tail; //儲存迴圈開始時, 當時的tail值
*         p= t; // 每次查詢的起始位置, 可能指向首節點head或者臨時尾節點t
*         q= p.next; // 每次迴圈下一個節點
*        newNode= new Node; // 新節點
*
*
* 重要概念:
*     當p = p.next時, 表示節點已經被移除
*/
public boolean offer(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        if (q == null) {    // 情況1:  p是尾節點
            // p is last node
            // p是尾節點就直接將新節點放入末尾
            if (p.casNext(null, newNode)) {
                // Successful CAS is the linearization point
                // for e to become an element of this queue,
                // and for newNode to become "live".
                if (p != t) // hop two nodes at a time // 一次跳兩個節點, 即插入兩次, tail更新一次
                    casTail(t, newNode);  // Failure is OK. // 失敗也無妨, 說明被別的執行緒更新了
                return true;  // 退出迴圈
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q)  // 情況2:  p節點被刪除了
            // We have fallen off list.  If tail is unchanged, it
            // will also be off-list, in which case we need to
            // jump to head, from which all live nodes are always
            // reachable.  Else the new tail is a better bet.
            // 儲存的節點p、t都已經失效了,這時需要重新檢索,重新檢索的起始位置有兩種情況
            //     1.1. 如果tail==t,表示tail也是失效的, 那麼從head開始找
            //     1.2. 否則tail就是被其他執行緒更新了, 可以又試著從tail找
            p = (t != (t = tail)) ? t : head;
        else             // 情況3:   沿著p往下找
            // Check for tail updates after two hops.
            // 這段簡單看作p = q就好理解了,  這麼寫是為了提高效率:
            //     1. 情況二中p可能指向了head(由於tail節點失效導致的)
            //     2. 現在tail可能被其他執行緒更新,也許重新指向了隊尾
            //     3. 如果是, 嘗試則從隊尾開始找, 以減少迭代次數
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

這兩段程式碼看了很久, 特別記錄下:

  1. 情況2中的p = (t != (t = tail)) ? t : head;
    (t != (t = tail))可以分三步來看

      1.1. 首先取出t
      1.2. 將tail賦值給t
      1.3. 將先前取出的t與更新後的t比較
    
  2. 情況3中p = (p != t && t != (t = tail)) ? t : q;

    首先: p != t: 這種情況只有可能發生在執行了情況2後
    現狀: 這時p指向head或者中間的元素, t指向一個被刪除了的節點
    那麼如果tail被其他執行緒更新了, 我們可以將t重新指向tail, p指向t, 就像剛進迴圈一樣, 從尾節點開始檢索。
    這樣比從head往後找更有效率

出隊poll

規則定義:

補充一項, item==null,也表示節點已經被刪除(參考remove方法)。

/**
* updateHead
*   
*/
public E poll() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;

            if (item != null && p.casItem(item, null)) {
                // Successful CAS is the linearization point
                // for item to be removed from this queue.
                if (p != h) // hop two nodes at a time
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            else if (p == q)
                continue restartFromHead;
            else
                p = q;
        }
    }
}

/**
 * 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);
}

出隊設值操作:

先更新head, 再將舊head的next指向自己

Note:

CAS演算法實現依靠Unsafe.compareAndSwapObject實現

UNSAFE.compareAndSwapObject(物件, 欄位偏移量, 當前值, 新值)

可以為物件中的某個欄位實現CAS操作

lazySet依賴UNSAFE.putOrderedObject實現

UNSAFE.putOrderedObject(物件, 欄位偏移量, 新值)

這個只能用在volatile欄位上
個人理解: volatile的設值會導致本地快取失效, 那麼需要重新從主存讀取, 使用這個方法可以使暫存器快取依舊有效, 不必急於從主存取值。
使用目的: 移除節點時, 需要更新節點的next指向自身, 但現在next指向的資料實際是有效的; 高併發情況下,如果offser方法已經快取了這個next值, 直接設定next會導致快取行失效, CPU需要重新讀取next; 而使用putOrderedObject可以讓offser從這個next繼續檢索

相關文章