ConcurrentLinkedQueue 原始碼分析 (基於Java 8)

weixin_34208283發表於2017-01-14
ConcurrentLinkedQueue

通過名字大家就可以知道, 這是一個通過連結串列實現的併發安全的佇列, 它應該是java中併發環境下效能最好的佇列, 為什麼呢? 因為它的不變性(invariants) 與可變性(non-invariants)

1. 基本原則不變性(fundamental invariants)
1.整個佇列中一定會存在一個 node(node.next = null), 並且僅存在一個, 但tail引用不一定指向它
2. 佇列中所有 item != null 的節點, head一定能夠到達; cas 設定 node.item = null, 意味著這個節點被刪除
head引用的不變性和可變性
不變性(invariants)
1. 所有的有效節點通過 succ() 方法都可達
2. head != null
3. (tmp = head).next != tmp || tmp != head (其實就是 head.next != head)
 
可變性(Non-invariants)
1. head.item 可能是 null, 也可能不是 null
2. 允許 tail 滯後於 head, 也就是呼叫 succ() 方法, 從 head 不可達tail
tail 引用的不變性和可變性
不變性(invariants)
1. tail 節點通過succ()方法一定到達佇列中的最後一個節點(node.next = null)
2. tail != null
 
可變性(Non-invariants)
1. tail.item 可能是 null, 也可能不是 null
2. 允許 tail 滯後於 head, 也就是呼叫 succ() 方法, 從 head 不可達tail
3. tail.next 可能指向 tail

這些不變性(invariants) 和 可變性(Non-invariants) 造成 ConcurrentLinkedQueue 有些異於一般queue的特點:

1. head 與 tail 都有可能指向一個 (item = null) 的節點
2. 如果 queue 是空的, 則所有 node.item = null
3. queue剛剛建立時 head = tail = dummyNode
4. head/tail 的 item/next 的操作都是通過 CAS

暈了, 是哇! 沒事, 這些都是特性, 我們先看程式碼, 回頭再回顧這些特性.

2. 內部節點 Node
import com.lami.tuomatuo.search.base.concurrent.unsafe.UnSafeClass;
import sun.misc.Unsafe;

/**
 * http://hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/classes/sun/misc/Unsafe.java
 * http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/9b0ca45cd756/src/share/vm/prims/unsafe.cpp
 * http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
 *
 * Created by xjk on 1/13/17.
 */
public class Node<E> {
    volatile E item;
    volatile Node<E> next;

    Node(E item){
        /**
         * Stores a reference value into a given Java variable.
         * <p>
         * Unless the reference <code>x</code> being stored is either null
         * or matches the field type, the results are undefined.
         * If the reference <code>o</code> is non-null, car marks or
         * other store barriers for that object (if the VM requires them)
         * are updated.
         * @see #putInt(Object, int, int)
         *
         * 將 Node 物件的指定 itemOffset 偏移量設定 一個引用值
         */
        unsafe.putObject(this, itemOffset, item);
    }

    boolean casItem(E cmp, E val){
        /**
         * Atomically update Java variable to <tt>x</tt> if it is currently
         * holding <tt>expected</tt>.
         * @return <tt>true</tt> if successful
         * 原子性的更新 item 值
         */
        return unsafe.compareAndSwapObject(this, itemOffset, cmp, val);
    }

    void lazySetNext(Node<E> val){
        /**
         * Version of {@link #putObjectVolatile(Object, long, Object)}
         * that does not guarantee immediate visibility of the store to
         * other threads. This method is generally only useful if the
         * underlying field is a Java volatile (or if an array cell, one
         * that is otherwise only accessed using volatile accesses).
         *
         * 呼叫這個方法和putObject差不多, 只是這個方法設定後對應的值的可見性不一定得到保證,
         * 這個方法能起這個作用, 通常是作用在 volatile field上, 也就是說, 下面中的引數 val 是被volatile修飾
         */
        unsafe.putOrderedObject(this, nextOffset, val);
    }

    /**
     * Atomically update Java variable to <tt>x</tt> if it is currently
     * holding <tt>expected</tt>.
     * @return <tt>true</tt> if successful
     *
     * 原子性的更新 nextOffset 上的值
     *
     */
    boolean casNext(Node<E> cmp, Node<E> val){
        return unsafe.compareAndSwapObject(this, nextOffset, cmp, val);
    }

    private static Unsafe unsafe;
    private static long itemOffset;
    private static long nextOffset;

    static {
        try {
            unsafe = UnSafeClass.getInstance();
            Class<?> k = Node.class;
            itemOffset = unsafe.objectFieldOffset(k.getDeclaredField("item"));
            nextOffset = unsafe.objectFieldOffset(k.getDeclaredField("next"));
        }catch (Exception e){

        }
    }
}

整個內部節點 Node 的程式碼比較簡單, 若不瞭解 Unsafe 類使用的, 請點選連結 Unsafe 與 LockSupport

3. ConcurrentLinkedQueue 內部屬性及構造方法
/** head 節點 */
private transient volatile Node<E> head;
/** tail 節點 */
private transient volatile Node<E> tail;

public ConcurrentLinkedList() {
    /** 預設會構造一個 dummy 節點
     * dummy 的存在是防止一些特殊複雜程式碼的出現 
     */
    head = tail = new Node<E>(null);
}

初始化 ConcurrentLinkedQueue時 head = tail = dummy node.

4. 查詢後繼節點方法 succ()
/**
 * 獲取 p 的後繼節點, 若 p.next = p (updateHead 操作導致的), 則說明 p 已經 fall off queue, 需要 jump 到 head
 */
final Node<E> succ(Node<E> p){
    Node<E> next = p.next;
    return (p == next)? head : next;
}

獲取一個節點的後繼節點不是 node.next 嗎, No, No, No, 還有特殊情況, 就是tail 指向一個哨兵節點 (node.next = node); 程式碼的註釋中我提到了 哨兵節點是 updateHead 導致的, 那我們來看 updateHead方法.

5. 特別的更新頭節點方法 updateHead

為什麼說 updateHead 特別呢? 還是看程式碼

/**
 * Tries to CAS head to p, If successfully, repoint old head to itself
 * as sentinel for succ(), blew
 *
 * 將節點 p設定為新的節點(這是原子操作),
 * 之後將原節點的next指向自己, 直接變成一個哨兵節點(為queue節點刪除及garbage做準備)
 *
 * @param h
 * @param p
 */
final void updateHead(Node<E> h, Node<E> p){
    if(h != p && casHead(h, p)){
        h.lazySetNext(h);
    }
}

主要這個 h.lazySetNext(h), 將 h.next -> h 直接變成一個哨兵節點, 這種lazySetNext主要用於無阻塞資料結構的 nulling out, 要了解詳情 點選 Unsafe 與 LockSupport
有了上面的這些輔助方法, 我們開始進入正題

6. 入佇列操作 offer()

一般我們的思維: 入隊操作就是 tail.next = newNode; 而這裡不同, 為什麼呢? 我們再來回顧一下 tail 的不變性和可變性

不變性(invariants)
1. tail 節點通過succ()方法一定到達佇列中的最後一個節點(node.next = null)
2. tail != null

可變性(Non-invariants)
1. tail.item 可能是 null, 也可能不是 null
2. 允許 tail 滯後於 head, 也就是呼叫 succ() 方法, 從 head 不可達tail
3. tail.next 可能指向 tail

主要是這裡 tail 會滯後於 head, 所以呢 要找到正真的 last node (node.next = null)
直接來程式碼

/**
 * Inserts the specified element at the tail of this queue
 * As the queue is unbounded, this method will never return {@code false}
 *
 * @param e {@code true} (as specified by {@link Queue#offer(Object)})
 * @return NullPointerException if the specified element is null
 *
 * 在佇列的末尾插入指定的元素
 */
public boolean offer(E e){
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e); // 1. 構建一個 node

    for(Node<E> t = tail, p = t;;){ // 2. 初始化變數 p = t = tail
        Node<E> q = p.next;  // 3. 獲取 p 的next
        if(q == null){      // q == null, 說明 p 是 last Node
            // p is last node
            if(p.casNext(null, newNode)){   // 4. 對 p 進行 cas 操作, newNode -> p.next
                // Successful CAS is the linearization point
                // for e to become an element of the queue,
                // and for newNode to become "live"
                if(p != t){ // 5. 每每經過一次 p = q 操作(向後遍歷節點), 則 p != t 成立, 這個也說明 tail 滯後於 head 的體現
                    casTail(t, newNode); // Failure is OK
                }
                return true;
            }
        }
        else if(p == q){  // 6. (p == q) 成立, 則說明p是pool()時呼叫 "updateHead" 導致的(刪除頭節點); 此時說明 tail 指標已經 fallen off queue, 所以進行 jump 操作, 若在t沒變化, 則 jump 到 head, 若 t 已經改變(jump操作在另外的執行緒中執行), 則jump到 head 節點, 直到找到 node.next = null 的節點
            /** 1. 大前提 p 是已經被刪除的節點
             *  2. 判斷 tail 是否已經改變
             *      1) tail 已經變化, 則說明 tail 已經重新定位
             *      2) tail 未變化, 而 tail 指向的節點是要刪除的節點, 所以讓 p 指向 head
             *  判斷尾節點是否有變化
             *  1. 尾節點變化, 則用新的尾節點
             *  2. 尾節點沒變化, 將 tail 指向head
             *
             *  public void test(){
             *        String tail = "";
             *        String t = (tail = "oldTail");
             *        tail = "newTail";
             *        boolean isEqual = t != (t = tail); // <- 神奇吧
             *        System.out.println("isEqual : "+isEqual); // isEqual : true
             *  }
             */
            p = (t != (t = tail))? t : head;
        }else{
            // 7. (p != t) -> 說明執行過 p = q 操作(向後遍歷操作), "(t != (t = tail)))" -> 說明尾節點在其他的執行緒發生變化
            // 為什麼 "(t != (t = tail)))" 一定要滿足呢, 因為 tail變更, 節省了 (p = q) 後 loop 中的無畏操作, tail 更新說明 q節點肯定也是無效的
            p = (p != t && (t != (t = tail))) ? t : q;
        }
    }
}

先瞄一下這段程式碼: 發現有3大疑惑:

  1. 明明 Node<E> q = p.next, 怎麼會有 p = q ?
  2. "p = (t != (t = tail))? t : head" 這段程式碼是什麼玩意, 是不是讓你直接懷疑自己的java基礎了, 不急我們慢慢來.
  3. 最後就是 "p = (p != t && (t != (t = tail))) ? t : q"

queue 初始化時是這樣的:

263562-36a7f6bd7293109c.png
state1.png

整個 queue 中 head = tail = dummyNode, 這時我們開始 offer 元素
1) 新增元素 a
1. 由於 head = tail = dummyNode, 所以 p.next = null
2. 直接操作步驟4 (p.casNext(null, newNode)), 若操作成功, 接著往下走, 不成功(併發時 其他的cas操作成功), 再loop 重試至成功
3. 判斷 p != t, 這時沒出現 tail指向的不是 last node,所以不成立, 直接return
新增元素a後:

263562-7951ee637a16e49f.png
state2.png
  1. 新增元素 b
    1. 此時還是 head = tail = dummyNode, p節點是 dummyNode, q.item = a, q.item != null 且 q != null, 直接執行步驟7 p = q (p != t && (t != (t = tail)) 下面說)
    2. 再次 判斷 q == null, 所以 有執行步驟4 p.casNext(), 這時因為執行過 p = q, 所以 p != t 成立, 對tail進行cas操作
    3. 最後直接 return
      新增 b 之後:
263562-e8dd734af04d0ef4.png
state3.png
  1. 新增元素c
    1. 這裡操作步驟和新增 a 一樣, 所以不說了
      新增c後:
263562-e3f4ecbfda4dd1ea.png
state4.png

解決上面的疑惑(看這裡時最好將下面的 poll也看一遍):

1. "p = q", 這是在poll方法中呼叫 updateHead 方法所致的 
2. "p = (t != (t = tail))", 這段程式碼的意思是 若 tail 節點在另外的節點中有變化 tail != t, 則將 tail 賦值給 p.雖然只有這短短一行程式碼, 但是包含非常多的意思:
   i!= 這個操作符號不是原子的, 它可以被中斷; 
   ii) 執行時 先獲取t的值, 再 t = tail, 賦值好了之後再與原來的t比較
   iii) 在多執行緒環境中 tail 很可能在上面新增元素的過程中被改變, 所以會出現 t != tail, 若tail被修改, 則用新的tail, 不然直接跳到head節點
3. 多了一個 p != t , 因為 tail變更, 節省了 (p = q) 後 loop 中的無畏操作, tail 更新說明 q節點肯定也是無效的

OK 至此 整個offer是分析好了, 接下來 poll

7. 出佇列操作 poll()

因為這個操作涉及 head 引用, 所以我們再來回顧一下head的不變性和可變性:

不變性(invariants)

1. 所有的有效節點通過 succ() 方法都可達
2. head != null
3. (tmp = head).next != tmp || tmp != head (其實就是 head.next != head)

可變性(Non-invariants)
1. head.item 可能是 null, 也可能不是 null
2. 允許 tail 滯後於 head, 也就是呼叫 succ() 方法, 從 head 不可達tail

head主要特點 tail 可能之後 head, 且head.item 可能是 null
不廢話了, 直接上程式碼

public E poll(){
    restartFromHead:
    for(;;){ // 0. 為啥這裡面是兩個 for 迴圈? 不防, 你去掉個試試, 其實主要是為了在 "continue restartFromHead" 後進行第二個 for loop 中的初始化
        for(Node<E> h = head, p = h, q;;){ // 1.進行變數的初始化 p = h = head,
            E item = p.item;

            if(item != null && p.casItem(item, null)){  // 2. 若 node.item != null, 則進行cas操作, cas成功則返回值
                // Successful CAS is the linearization point
                // for item to be removed from this queue
                if(p != h){ // hop two nodes at a time  // 3. 若此時的 p != h, 則更新 head(那啥時 p != h, 額, 這個絕對坑啊 -> 執行第8步後)
                    updateHead(h, ((q = p.next) != null)? q : p); // 4. 進行 cas 更新 head ; "(q = p.next) != null" 怕出現p此時是尾節點了; 在 ConcurrentLinkedQueue 中正真的尾節點只有1個(必須滿足node.next = null)
                }
                return item;
            }
            else if((q = p.next) == null){  // 5. queue是空的, p是尾節點
                updateHead(h, p); // 6. 這一步除了更新head 外, 還是helpDelete刪除佇列操作, 刪除 p 之前的節點(和 ConcurrentSkipListMap.Node 中的 helpDelete 有異曲同工之妙)
                return null;
            }
            else if(p == q){ // 7. p == q -> 說明 p節點已經是刪除了的head節點, 為啥呢?(見updateHead方法)
                continue restartFromHead;
            }else
                p = q; // 8. 將 q -> p, 進行下個節點的 poll 操作(初始化一個 dummy 節點, 在單執行緒情況下, 這個 if 判斷是第一個執行的)
        }
    }
}

理解了offer之後我想 poll 應該比較簡單了.
我們再來回顧一下剛剛新增了 a, b, c, 之後佇列的狀態:

263562-37b9946794084262.png
state4.png
  1. poll 第一個元素 a
1. 此時 head指向 dummy, tail 指向 item = b 的節點, 所以在步驟2中 item == null, 而 (q = p.next) != null, 所以直接跳到步驟8, 
2. 這時 p指向a, 且滿足 item != null, 所以執行步驟2, 又因為執行了步驟8, 所以 p != h, 進行 head 節點的更新 (head 指向這時p.next節點)

poll item = a 後:

263562-3ec9bd782e1ad611.png
state6.png
  1. poll 第二個元素 b
1. 此時 head = tail = b 節點, 所以 item != null, 直接執行 步驟2, 而 p == h , 所以不更新head

poll 節點 b 後:

263562-d9bd830e11bce65e.png
state7.png
  1. poll 第三個元素 c
    poll 節點 c 和 poll 節點啊一樣的, 所以不說了, 直接看結果圖
263562-90b0954fb9383dd9.png
state8.png

一目瞭然, tail 滯後於 head

  1. ok 這時我們再進行 offer() 節點 d, 則就會出現 offer 中的步驟 6 (p == q), 所以這時p直接跳到 head節點, 來進行更新, 步驟省略....

結果如圖 :

263562-fbb3788bb4047ea8.png
state9.png

至此整個 poll 分析結束

8. 總結

ConcurrentLinkedQueue 的整個設計十分精妙, 它使用 CAS 處理對資料的操作, 同時允許佇列處於不一致的狀態; 這種特性分離了一般 poll/offer時需要兩個原子的操作, 對了尤其是節點的刪除 (updateHead) 和後繼節點的訪問 succ(), 而對 ConcurrentLinkedQueue的掌握有助於我們瞭解 SynchronousQueue, AQS, FutureTask 中的 Queue

參考資料:
Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue
vickyqi ConcurrentLinkedQueue
大飛 ConcurrentLinkedQueue

相關文章