源碼|并發(fā)一枝花之ConcurrentLinkedQueue【偽】

首先聲明,本文是偽源碼分析。主要是基于狀態(tài)機(jī)自己實(shí)現(xiàn)一個(gè)簡(jiǎn)化的并發(fā)隊(duì)列,有助于讀者掌握并發(fā)程序設(shè)計(jì)的核心——狀態(tài)機(jī);最后對(duì)源碼實(shí)現(xiàn)略有提及。

ConcurrentLinkedQueue不支持阻塞,沒有BlockingQueue那么易用;但在中等規(guī)模的并發(fā)場(chǎng)景下,其性能卻比BlockingQueue高不少,而且相當(dāng)穩(wěn)定。同時(shí),ConcurrentLinkedQueue是學(xué)習(xí)CAS的經(jīng)典案例。根據(jù)github的code results排名,ConcurrentLinkedQueue(164k)也十分流行,比我想象中的使用量大多了。非常值得一講。

對(duì)于狀態(tài)機(jī)和并發(fā)程序設(shè)計(jì)的基本理解,可以參考源碼|并發(fā)一枝花之BlockingQueue,建議第一次接觸狀態(tài)機(jī)的同學(xué)速讀參考文章之后,再來閱讀此文章。

JDK版本:oracle java 1.8.0_102

準(zhǔn)備知識(shí):CAS

讀者可以跳過這部分,后面講到offer()方法的實(shí)現(xiàn)時(shí)再回顧。

悲觀鎖與樂觀鎖

  • 悲觀鎖:假定并發(fā)環(huán)境是悲觀的,如果發(fā)生并發(fā)沖突,就會(huì)破壞一致性,所以要通過獨(dú)占鎖徹底禁止沖突發(fā)生。有一個(gè)經(jīng)典比喻,“如果你不鎖門,那么搗蛋鬼就回闖入并搞得一團(tuán)糟”,所以“你只能一次打開門放進(jìn)一個(gè)人,才能時(shí)刻盯緊他”。
  • 樂觀鎖:假定并發(fā)環(huán)境是樂觀的,即,雖然會(huì)有并發(fā)沖突,但沖突可發(fā)現(xiàn)且不會(huì)造成損害,所以,可以不加任何保護(hù),等發(fā)現(xiàn)并發(fā)沖突后再?zèng)Q定放棄操作還是重試。可類比的比喻為,“如果你不鎖門,那么雖然搗蛋鬼會(huì)闖入,但他們一旦打算破壞你就能知道”,所以“你大可以放進(jìn)所有人,等發(fā)現(xiàn)他們想破壞的時(shí)候再做決定”。

通常認(rèn)為樂觀鎖的性能比悲觀所更高,特別是在某些復(fù)雜的場(chǎng)景。這主要由于悲觀鎖在加鎖的同時(shí),也會(huì)把某些不會(huì)造成破壞的操作保護(hù)起來;而樂觀鎖的競(jìng)爭(zhēng)則只發(fā)生在最小的并發(fā)沖突處,如果用悲觀鎖來理解,就是“鎖的粒度最小”。但樂觀鎖的設(shè)計(jì)往往比較復(fù)雜,因此,復(fù)雜場(chǎng)景下還是多用悲觀鎖。

首先保證正確性,有必要的話,再去追求性能。

CAS

樂觀鎖的實(shí)現(xiàn)往往需要硬件的支持,多數(shù)處理器都都實(shí)現(xiàn)了一個(gè)CAS指令,實(shí)現(xiàn)“Compare And Swap”的語義(這里的swap是“換入”,也就是set),構(gòu)成了基本的樂觀鎖。

CAS包含3個(gè)操作數(shù):

  • 需要讀寫的內(nèi)存位置V
  • 進(jìn)行比較的值A(chǔ)
  • 擬寫入的新值B

當(dāng)且僅當(dāng)位置V的值等于A時(shí),CAS才會(huì)通過原子方式用新值B來更新位置V的值;否則不會(huì)執(zhí)行任何操作。無論位置V的值是否等于A,都將返回V原有的值。

一個(gè)有意思的事實(shí)是,“使用CAS控制并發(fā)”與“使用樂觀鎖”并不等價(jià)。CAS只是一種手段,既可以實(shí)現(xiàn)樂觀鎖,也可以實(shí)現(xiàn)悲觀鎖。樂觀、悲觀只是一種并發(fā)控制的策略。下文將分別用CAS實(shí)現(xiàn)悲觀鎖和樂觀鎖?
我們先不講JDK提供的實(shí)現(xiàn),用狀態(tài)機(jī)模型來分析一下,看我們能不能自己實(shí)現(xiàn)一版。

隊(duì)列的狀態(tài)機(jī)模型

狀態(tài)機(jī)模型與是否需要并發(fā)無關(guān),一個(gè)類不管是否是線程安全的,其狀態(tài)機(jī)模型從類被實(shí)現(xiàn)(此時(shí),所有類行為都是確定的)開始就是確定的。接口是類行為的一個(gè)子集,我們從接口出發(fā),逐漸構(gòu)建出簡(jiǎn)化版ConcurrentLinkedQueue的狀態(tài)機(jī)模型。

隊(duì)列接口

ConcurrentLinkedQueue實(shí)現(xiàn)了Queue接口:

public interface BlockingQueue<E> extends Queue<E> {
  boolean add(E e);

  boolean offer(E e);

  E remove();

  E poll();

  E element();
  
  E peek();
}

需要關(guān)注的是一對(duì)方法:

  • offer():入隊(duì),成功返回true,失敗返回false。JDK中ConcurrentLinkedQueue實(shí)現(xiàn)為無界隊(duì)列,這里我們也只討論無界的情況——因此,offer()方法必返回true。
  • poll():出隊(duì),有元素返回元素,沒有就返回null。

同時(shí),理想的線程安全隊(duì)列中,入隊(duì)和出隊(duì)之間不應(yīng)該存在競(jìng)爭(zhēng),這樣入隊(duì)的狀態(tài)機(jī)模型和出隊(duì)的狀態(tài)機(jī)模型可以完全解耦,互不影響。

對(duì)我們的狀態(tài)機(jī)作出兩個(gè)假設(shè):

  • 假設(shè)1:只支持這入隊(duì)、出隊(duì)兩種行為。
  • 假設(shè)2:入隊(duì)、出隊(duì)之間不存在競(jìng)爭(zhēng),即入隊(duì)模型與出隊(duì)模型是對(duì)偶、獨(dú)立的兩個(gè)狀態(tài)機(jī)。

從而,可以先分析入隊(duì),再參照分析出隊(duì);然后可嘗試去掉假設(shè)2,看如何完善我們的實(shí)現(xiàn)來保證假設(shè)2成立;最后看看真·神Doug Lea如何實(shí)現(xiàn),學(xué)習(xí)一波

狀態(tài)機(jī)定義

現(xiàn)在基于假設(shè)1和假設(shè)2,嘗試定義入隊(duì)模型的狀態(tài)機(jī)。

我們構(gòu)造一個(gè)簡(jiǎn)化的場(chǎng)景:存在2個(gè)生產(chǎn)者P1、P2,同時(shí)觸發(fā)入隊(duì)操作

狀態(tài)集

如果是單線程環(huán)境,入隊(duì)操作將是這樣的:

// 準(zhǔn)備
newNode.next = null;
curTail = tail;

// 入隊(duì)前
assert tail == curTail && tail.next == null; // 狀態(tài)S1
// 開始入隊(duì)
tail.next = newNode; // 事件E1
// 入隊(duì)中
assert tail == curTail && tail.next == newNode; // 狀態(tài)S2
tail = tail.next; // 事件E2
// 結(jié)束入隊(duì)
// 入隊(duì)后
assert tail == newNode && tail.next == null; // 狀態(tài)S3,合并到狀態(tài)S1

該過程涉及對(duì)兩個(gè)域的修改:tail.next、tail。則隨著操作的進(jìn)行,隊(duì)列會(huì)經(jīng)歷2種狀態(tài):

  • 狀態(tài)S1:事件E1執(zhí)行前,tail指向?qū)嶋H的尾節(jié)點(diǎn)curTail,tail.next==null。如生產(chǎn)者P1、P2都還沒有觸發(fā)入隊(duì)時(shí),隊(duì)列處于狀態(tài)S1;生產(chǎn)者P1完成入隊(duì)P2還沒觸發(fā)入隊(duì)時(shí),隊(duì)列處于狀態(tài)S1。
  • 狀態(tài)S2:事件E1執(zhí)行后、E2執(zhí)行前,tail指向舊的尾節(jié)點(diǎn)curTail,tail.next==newNode。
  • 狀態(tài)S3:事件E2執(zhí)行后,tail指向新的尾節(jié)點(diǎn)newNode,tail.next==null。同狀態(tài)S1,合并。

狀態(tài)轉(zhuǎn)換集

兩個(gè)事件分別對(duì)應(yīng)兩個(gè)狀態(tài)轉(zhuǎn)換:

  • 狀態(tài)轉(zhuǎn)換T1:S1->S2,即tail.next = newNode。
  • 狀態(tài)轉(zhuǎn)換T2:S2->S1,即tail = tail.next。

是不是很熟悉?因?yàn)镃oncurrentLinkedQueue也是隊(duì)列,必然同BlockingQueue相似甚至相同。區(qū)別在于如何維護(hù)這些狀態(tài)和狀態(tài)轉(zhuǎn)換。

自擼ConcurrentLinkedQueue

依賴CAS,兩個(gè)狀態(tài)轉(zhuǎn)換T1、T2都可以實(shí)現(xiàn)為原子操作。留給我們的問題是,如何維護(hù)合法的狀態(tài)轉(zhuǎn)換。

入隊(duì)方法offer()

入隊(duì)過程需要經(jīng)過兩個(gè)狀態(tài)轉(zhuǎn)換,且這兩個(gè)狀態(tài)轉(zhuǎn)換必須連續(xù)發(fā)生。

不嚴(yán)謹(jǐn)。“連續(xù)”并不是必要的,最后分析源碼的時(shí)候會(huì)看到。不過,我們暫時(shí)使用強(qiáng)一致性的模型。

思路1:讓同一個(gè)生產(chǎn)者P1連續(xù)完成兩個(gè)狀態(tài)轉(zhuǎn)換T1、T2,保證P2不會(huì)插入進(jìn)來

LinkedBlockingQueue的思路即是如此。這是一種悲觀策略——一次開門只放進(jìn)來一個(gè)生產(chǎn)者,似乎只能像LinkedBlockingQueue那樣,用傳統(tǒng)的鎖putLock實(shí)現(xiàn),實(shí)際上,依靠CAS也能實(shí)現(xiàn):

public class ConcurrentLinkedQueue1<E> {
  private volatile Node<E> tail;

  public ConcurrentLinkedQueue1() {
    throw new UnsupportedOperationException("Not implement");
  }

  public boolean offer(E e) {
    Node<E> newNode = new Node<E>(e, new AtomicReference<>(null));
    while (true) {
      Node<E> curTail = tail;
      AtomicReference<Node<E>> curNext = curTail.next;
      // 嘗試T1:CAS設(shè)置tail.next
      if (curNext.compareAndSet(null, newNode)) {
        // 成功者視為獲得獨(dú)占鎖,完成了T1。直接執(zhí)行T2:設(shè)置tail
        tail = curNext.get();
        return true;
      }
      // 失敗者自旋等待
    }
  }

  private static class Node<E> {
    private volatile E item;
    private AtomicReference<Node<E>> next;

    public Node(E item, AtomicReference<Node<E>> next) {
      this.item = item;
      this.next = next;
    }
  }
}

思路2:生產(chǎn)者P1完成狀態(tài)轉(zhuǎn)換T1后,P2代勞完成狀態(tài)轉(zhuǎn)換T2

再來分析下T1、T2兩個(gè)狀態(tài)轉(zhuǎn)換:

  • T1涉及newNode,只能由封閉持有newNode的生產(chǎn)者P1完成
  • T2只涉及隊(duì)列中的信息,任何持有隊(duì)列的生產(chǎn)者都有能力完成。P1可以,P2也可以

思路1是悲觀的,認(rèn)為T1、T2必須都由P1完成,如果P2插入就會(huì)“搞破壞”。而思路2則打開大門,歡迎任何“有能力”的生產(chǎn)者完成T2,是典型的樂觀策略。

public class ConcurrentLinkedQueue2<E> {
  private AtomicReference<Node<E>> tail;

  public ConcurrentLinkedQueue2() {
    throw new UnsupportedOperationException("Not implement");
  }

  public boolean offer(E e) {
    Node<E> newNode = new Node<E>(e, new AtomicReference<>(null));
    while (true) {
      Node<E> curTail = tail.get();
      AtomicReference<Node<E>> curNext = curTail.next;
      // 嘗試T1:CAS設(shè)置tail.next
      if (curNext.compareAndSet(null, newNode)) {
        // 成功者完成了T1,隊(duì)列處于S2,繼續(xù)嘗試T2:CAS設(shè)置tail
        tail.compareAndSet(curTail, curNext.get());
        // 成功表示該生產(chǎn)者P1完成連續(xù)完成了T1、T2,隊(duì)列處于S1
        // 失敗表示T2已經(jīng)由生產(chǎn)者P2完成,隊(duì)列處于S1
        return true;
      }
      // 失敗者得知隊(duì)列處于S2,則嘗試T2:CAS設(shè)置tail
      tail.compareAndSet(curTail, curNext.get());
      // 如果成功,隊(duì)列轉(zhuǎn)換到S1;如果失敗,隊(duì)列表示T2已經(jīng)由生產(chǎn)者P1完成,隊(duì)列已經(jīng)處于S1
      // 然后循環(huán),重新嘗試T1
    }
  }

  private static class Node<E> {
    private volatile E item;
    private AtomicReference<Node<E>> next;

    public Node(E item, AtomicReference<Node<E>> next) {
      this.item = item;
      this.next = next;
    }
  }
}

減少無效的競(jìng)爭(zhēng)

我們涉及的狀態(tài)比較少(只有2個(gè)狀態(tài)),繼續(xù)看看能否減少無效的競(jìng)爭(zhēng),比如:

  • 前兩種實(shí)現(xiàn)的第一步都是CAS嘗試T1,失敗了就退化成一次探查(compare and swap中的compare)。發(fā)起CAS前,可能隊(duì)列已經(jīng)處于S2,這時(shí)CAS嘗試T1就成了浪費(fèi),只需要探查即可。這有點(diǎn)像DCL單例的思路(面試中單例模式有幾種寫法?),可以直接通過tail.next判斷隊(duì)列是否處于S1,來完成一部分探查,以減少無效的競(jìng)爭(zhēng)
public class ConcurrentLinkedQueue3<E> {
  private AtomicReference<Node<E>> tail;

  public ConcurrentLinkedQueue3() {
    throw new UnsupportedOperationException("Not implement");
  }

  public boolean offer(E e) {
    Node<E> newNode = new Node<E>(e, new AtomicReference<>(null));
    while (true) {
      Node<E> curTail = tail.get();
      AtomicReference<Node<E>> curNext = curTail.next;

      // 先檢查一下隊(duì)列狀態(tài)的狀態(tài),tail.next==null表示隊(duì)列處于狀態(tài)S1,僅此時(shí)才有CAS嘗試T1的必要
      if (curNext.get() == null) {
        // 如果處于S1,嘗試T1:CAS設(shè)置tail.next
        if (curNext.compareAndSet(null, newNode)) {
          // 成功者完成了T1,隊(duì)列處于S2,繼續(xù)嘗試T2:CAS設(shè)置tail
          tail.compareAndSet(curTail, curNext.get());
          // 成功表示該生產(chǎn)者P1完成連續(xù)完成了T1、T2,隊(duì)列處于S1
          // 失敗表示T2已經(jīng)由生產(chǎn)者P2完成,隊(duì)列處于S1
          return true;
        }
      }
      // 否則隊(duì)列處于處于S2,或CAS嘗試T1的失敗者得知隊(duì)列處于S2,則嘗試T2:CAS設(shè)置tail
      tail.compareAndSet(curTail, curNext.get());
      // 如果成功,隊(duì)列轉(zhuǎn)換到S1;如果失敗,隊(duì)列表示T2已經(jīng)由生產(chǎn)者P1完成,隊(duì)列已經(jīng)處于S1
      // 然后循環(huán),重新嘗試T1
    }
  }

  private static class Node<E> {
    private volatile E item;
    private AtomicReference<Node<E>> next;

    public Node(E item, AtomicReference<Node<E>> next) {
      this.item = item;
      this.next = next;
    }
  }
}

注意,上述實(shí)現(xiàn)中,while代碼塊后都沒有返回值。這是被編譯器允許的,因?yàn)榫幾g器可以分析出,該方法不可能運(yùn)行到while代碼塊之后,所以while代碼塊后的返回值語句也是無效的。

出隊(duì)方法poll()

對(duì)偶的構(gòu)造一個(gè)簡(jiǎn)化的場(chǎng)景:存在2個(gè)消費(fèi)者C1、C2,同時(shí)觸發(fā)出隊(duì)操作

不需要考慮悲觀策略和優(yōu)化方案,我們嘗試基于思路2的第一種實(shí)現(xiàn)擼一版基礎(chǔ)的poll()方法。

然后,,,沒擼動(dòng)。想了一下,樸素鏈表(如LinkedList)中,直接用head表示維護(hù)頭結(jié)點(diǎn)無法區(qū)分“已取出item未移動(dòng)head指針”和“未取出item未移動(dòng)head指針”(同“已取出item已移動(dòng)head指針”)兩種狀態(tài)。所以還是寫一寫才知道深淺啊,碰巧前兩天寫了BlockingQueue的分析,dummy node正好派上用場(chǎng)。

隊(duì)列初始化如下:

dummy = new Node(null, null);
// tail = dummy; // 后面會(huì)用到
// head = dummy.next; // dummy.next 表示實(shí)際的頭結(jié)點(diǎn),但我們不需要存儲(chǔ)它

狀態(tài)機(jī)

單線程環(huán)境的出隊(duì)過程:

// 準(zhǔn)備
curDummy = dummy;
curNext = curDummy.next;
oldItem = curNext.item;

// 出隊(duì)前
assert dummy == curDummy && dummy.next.item == oldItem; // 狀態(tài)S1
// 開始出隊(duì)
dummy.next.item = null; // 事件E1
// 出隊(duì)中
assert dummy == curDummy && dummy.next.item == null; // 狀態(tài)S2
dummy = dummy.next; // 事件E2
// 結(jié)束出隊(duì)
// 出隊(duì)后
assert dummy == curNext && dummy.next.item != null; // 狀態(tài)S3,合并到狀態(tài)S1

狀態(tài):

  • 狀態(tài)S1:事件E1執(zhí)行前,dummy指向?qū)嶋H的dummy節(jié)點(diǎn)curDummy,dummy.next.item== oldItem。如消費(fèi)者C1、C2都還沒有觸發(fā)出隊(duì)時(shí),隊(duì)列處于狀態(tài)S1;消費(fèi)者C1完成入隊(duì)C2還沒觸發(fā)出隊(duì)時(shí),隊(duì)列處于狀態(tài)S1。
  • 狀態(tài)S2:事件E1執(zhí)行后、E2執(zhí)行前,dummy指向舊的dummy節(jié)點(diǎn)curDummy,dummy.next.item==null。
  • 狀態(tài)S3:事件E2執(zhí)行后,dummy指向新的dummy節(jié)點(diǎn)curNext,dummy.next.item!=null。這在本質(zhì)上同狀態(tài)S1是一致的,合并。

狀態(tài)轉(zhuǎn)換:

  • 狀態(tài)轉(zhuǎn)換T1:S1->S2,即dummy.next.item = null。
  • 狀態(tài)轉(zhuǎn)換T2:S2->S1,即dummy = dummy.next。

代碼

public class ConcurrentLinkedQueue4<E> {
  private AtomicReference<Node<E>> dummy;

  public ConcurrentLinkedQueue4() {
    dummy = new AtomicReference<>(new Node<>(null, null));
  }

  public E poll() {
    while (true) {

      Node<E> curDummy = dummy.get();
      Node<E> curNext = curDummy.next;
      E oldItem = curNext.item.get();
      // 嘗試T1:CAS設(shè)置dummy.next.item
      if (curNext.item.compareAndSet(oldItem, null)) {
        // 成功者完成了T1,隊(duì)列處于S2,繼續(xù)嘗試T2:CAS設(shè)置dummy
        dummy.compareAndSet(curDummy, curNext);
        // 成功表示該消費(fèi)者C1完成連續(xù)完成了T1、T2,隊(duì)列處于S1
        // 失敗表示T2已經(jīng)由消費(fèi)者C2完成,隊(duì)列處于S1
        return oldItem;
      }
      // 失敗者得知隊(duì)列處于S2,則嘗試T2:CAS設(shè)置dummy
      dummy.compareAndSet(curDummy, curNext);
      // 如果成功,隊(duì)列轉(zhuǎn)換到S1;如果失敗,隊(duì)列表示T2已經(jīng)由消費(fèi)者P1完成,隊(duì)列已經(jīng)處于S1
      // 然后循環(huán),重新嘗試T1
    }
  }

  private static class Node<E> {
    private AtomicReference<E> item;
    private volatile Node<E> next;

    public Node(AtomicReference<E> item, Node<E> next) {
      this.item = item;
      this.next = next;
    }
  }
}

另一種狀態(tài)機(jī)

實(shí)際上,前面的討論有意回避了一個(gè)問題——如果入隊(duì)/出隊(duì)操作順序不同,我們會(huì)構(gòu)造出不同的狀態(tài)機(jī)。這相當(dāng)于同一個(gè)類的另一種實(shí)現(xiàn),不違反前面作出的聲明:

狀態(tài)機(jī)模型與是否需要并發(fā)無關(guān),一個(gè)類不管是否是線程安全的,其狀態(tài)機(jī)模型從類被實(shí)現(xiàn)(此時(shí),所有類行為都是確定的)開始就是確定的。

繼續(xù)以出隊(duì)為例,假設(shè)在單線程下,采用這樣的順序出隊(duì):

// 準(zhǔn)備
curDummy = dummy;
curNext = curDummy.next;
oldItem = curNext.item;

// 出隊(duì)前
assert dummy == curDummy && dummy.item == null; // 狀態(tài)S1

// 開始出隊(duì)
dummmy = dummy.next; // 事件E1
// 出隊(duì)中
assert dummy == curNext && dummy.item == oldItem; // 狀態(tài)S2
dummy.item = null; // 事件E2
// 結(jié)束出隊(duì)

// 出隊(duì)后
assert dummy == curNext && dummy.item == null; // 狀態(tài)S3,合并到狀態(tài)S1

看起來,這樣的操作順序更容易定義各狀態(tài):

  • 狀態(tài)S1:事件E1執(zhí)行前,dummy指向?qū)嶋H的dummy節(jié)點(diǎn)curDummy,dummy.item == null。如消費(fèi)者C1、C2都還沒有觸發(fā)出隊(duì)時(shí),隊(duì)列處于狀態(tài)S1;消費(fèi)者C1完成入隊(duì)C2還沒觸發(fā)出隊(duì)時(shí),隊(duì)列處于狀態(tài)S1。
  • 狀態(tài)S2:事件E1執(zhí)行后、E2執(zhí)行前,dummy指向新的dummy節(jié)點(diǎn)curNext,dummy.item == oldItem。
  • 狀態(tài)S3:事件E2執(zhí)行后,dummy指向新的dummy節(jié)點(diǎn)curNext,dummy.item == null。顯然同狀態(tài)S1,合并。

狀態(tài)轉(zhuǎn)換:

  • 狀態(tài)轉(zhuǎn)換T1:S1->S2,即dummmy = dummy.next。
  • 狀態(tài)轉(zhuǎn)換T2:S2->S1,即dummy.item = null。

實(shí)現(xiàn)如下:

public class ConcurrentLinkedQueue5<E> {
  private AtomicReference<Node<E>> dummy;

  public ConcurrentLinkedQueue5() {
    dummy = new AtomicReference<>(new Node<>(null, null));
  }

  public E poll() {
    while (true) {

      Node<E> curDummy = dummy.get();
      Node<E> curNext = curDummy.next;
      E oldItem = curNext.item.get();
      // 嘗試T1:CAS設(shè)置dummmy
      if (dummy.compareAndSet(curDummy, curNext)) {
        // 成功者完成了T1,隊(duì)列處于S2,繼續(xù)嘗試T2:CAS設(shè)置dummy.item
        curDummy.item.compareAndSet(oldItem, null);
        // 成功表示該消費(fèi)者C1完成連續(xù)完成了T1、T2,隊(duì)列處于S1
        // 失敗表示T2已經(jīng)由消費(fèi)者C2完成,隊(duì)列處于S1
        return oldItem;
      }
      // 失敗者得知隊(duì)列處于S2,則嘗試T2:CAS設(shè)置dummy.item
      curDummy.item.compareAndSet(oldItem, null);
      // 如果成功,隊(duì)列轉(zhuǎn)換到S1;如果失敗,隊(duì)列表示T2已經(jīng)由消費(fèi)者P1完成,隊(duì)列已經(jīng)處于S1
      // 然后循環(huán),重新嘗試T1
    }
  }

  private static class Node<E> {
    private AtomicReference<E> item;
    private volatile Node<E> next;

    public Node(AtomicReference<E> item, Node<E> next) {
      this.item = item;
      this.next = next;
    }
  }
}

一個(gè)trick

實(shí)現(xiàn)上面狀態(tài)機(jī)的過程中,我想出了一個(gè)針對(duì)出隊(duì)操作的trick:可以去掉dummy node,用head維護(hù)頭結(jié)點(diǎn)+一步狀態(tài)轉(zhuǎn)換完成出隊(duì)

對(duì)啊,我寫著寫著又?jǐn)]出來了。。。

去掉了dummy node,那么head.item的初始狀態(tài)就是非空的,下面是簡(jiǎn)化的狀態(tài)機(jī)。

單線程出隊(duì)的操作順序:

// 準(zhǔn)備
curHead = head;
curNext = curHead.next;
oldItem = curHead.item;

// 出隊(duì)前
assert head == curHead; // 狀態(tài)S1

// 出隊(duì)
head = head.next; // 事件E1

// 出隊(duì)后
assert head == curNext; // 狀態(tài)S2,合并到狀態(tài)S1

出隊(duì)只需要嘗試head后移,成功者可從舊的頭結(jié)點(diǎn)curHead中取出item,之后curHead將被廢棄;失敗者再重新嘗試即可。如果在嘗試前就得到了item的引用,那么E1發(fā)生后,不管成功與否,在curHead上做什么都是無所謂的了,因?yàn)槭聦?shí)上沒有任何消費(fèi)者會(huì)再去訪問它。

這是一個(gè)單狀態(tài)的狀態(tài)機(jī),則狀態(tài):

  • 狀態(tài)S1:head指向?qū)嶋H的頭節(jié)點(diǎn)curHead。隊(duì)列始終處于狀態(tài)S1。
  • 狀態(tài)S2:head指向新的頭節(jié)點(diǎn)curNext。同S1,合并

狀態(tài)轉(zhuǎn)換:

  • 狀態(tài)轉(zhuǎn)換T1:S1->S1,即head = head.next。

實(shí)現(xiàn)如下:

public class ConcurrentLinkedQueue6<E> {
  private AtomicReference<Node<E>> head;

  public ConcurrentLinkedQueue6() {
    throw new UnsupportedOperationException("Not implement");
  }

  public E poll() {
    while (true) {

      Node<E> curHead = head.get();
      Node<E> curNext = curHead.next;
      // 嘗試T1:CAS設(shè)置head
      if (head.compareAndSet(curHead, curNext)) {
        // 成功者完成了T1,隊(duì)列處于S1
        return curHead.item; // 只讓成功者取出item
      }
      // 失敗者重試嘗試
    }
  }

  private static class Node<E> {
    private volatile E item;
    private volatile Node<E> next;

    public Node(E item, Node<E> next) {
      this.item = item;
      this.next = next;
    }
  }
}

其他特殊情況

前面都是基于假設(shè)2“入隊(duì)、出隊(duì)無競(jìng)爭(zhēng)”討論的。現(xiàn)在需要放開假設(shè)2,看如何完善已有的實(shí)現(xiàn)以保證假設(shè)2成立。或者如果不能保證假設(shè)2的話,如何解決競(jìng)爭(zhēng)問題。

根據(jù)對(duì)LinkedBlockingQueue的分析,我們得知,如果底層數(shù)據(jù)結(jié)構(gòu)是樸素鏈表,那么隊(duì)列空或長(zhǎng)度為1的時(shí)候,head、tail都指向同一個(gè)節(jié)點(diǎn)(或都為null),這時(shí)必然存在競(jìng)爭(zhēng);dummy node較好的解決了這一問題。ConcurrentLinkedQueue4是基于dummy node的方案,我們嘗試在此基礎(chǔ)上修改。

回顧dummy node的使用方法(配合ConcurrentLinkedQueue2和ConcurrentLinkedQueue4做了調(diào)整和精簡(jiǎn)):

  • 初始化鏈表時(shí),創(chuàng)建dummy node:
    • dummy = new Node(null, null)
    • // head = dummy.next // head 為 null <=> 隊(duì)列空
    • tail = dummy // tail.item 為 null <=> 隊(duì)列空
  • 在隊(duì)尾入隊(duì)時(shí),tail后移:
    • tail.next = new Node(newItem, null)
    • tail = tail.next
  • 在隊(duì)頭出隊(duì)時(shí),dummy后移,同步更新head:
    • oldItem = dummy.next.item // == head.item
    • dummy.next.item = null
    • dummy = dummy.next
    • // head = dummy.next
    • return oldItem

下面分情況討論。

case1:隊(duì)列空

隊(duì)列空時(shí),隊(duì)列處于一個(gè)特殊的狀態(tài),從該狀態(tài)出發(fā),僅能完成入隊(duì)相關(guān)的狀態(tài)轉(zhuǎn)換——通俗講就是隊(duì)列空時(shí)只允許入隊(duì)操作。這時(shí)消除競(jìng)爭(zhēng)很簡(jiǎn)單,只允許入隊(duì)不允許出隊(duì)即可:

public class ConcurrentLinkedQueue7<E> {
  private AtomicReference<Node<E>> dummy;
  private AtomicReference<Node<E>> tail;

  public ConcurrentLinkedQueue7() {
    Node<E> initNode = new Node<E>(
        new AtomicReference<E>(null), new AtomicReference<Node<E>>(null));
    dummy = new AtomicReference<>(initNode);
    tail = new AtomicReference<>(initNode);
    // Node<E> head = dummy.get().next.get();
  }

  public boolean offer(E e) {
    Node<E> newNode = new Node<E>(new AtomicReference<>(e), new AtomicReference<>(null));
    while (true) {
      Node<E> curTail = tail.get();
      AtomicReference<Node<E>> curNext = curTail.next;
      if (curNext.compareAndSet(null, newNode)) {
        tail.compareAndSet(curTail, curNext.get());
        return true;
      }
      tail.compareAndSet(curTail, curNext.get());
    }
  }

  public E poll() {
    while (true) {
      Node<E> curDummy = dummy.get();
      Node<E> curNext = curDummy.next.get();
      // 既可以用 dummy.next == null (head) 判空,也可以用 tail.item == null
      // 不過鑒于處于poll()方法中,使用 dummy.next 可讀性更好
      if (curNext == null) {
        return null;
      }
      E oldItem = curNext.item.get();
      if (curNext.item.compareAndSet(oldItem, null)) {
        dummy.compareAndSet(curDummy, curNext);
        return oldItem;
      }
      dummy.compareAndSet(curDummy, curNext);
    }
  }

  private static class Node<E> {
    private AtomicReference<E> item;
    private AtomicReference<Node<E>> next;

    public Node(AtomicReference<E> item, AtomicReference<Node<E>> next) {
      this.item = item;
      this.next = next;
    }
  }
}

ConcurrentLinkedQueue7需要原子的操作item和next,因此Node的item、next域都被聲明為了AtomicReference。

隊(duì)列空的時(shí)候:offer()方法同ConcurrentLinkedQueue2#offer(),不需要做特殊處理;poll()方法在ConcurrentLinkedQueue4#poll()的基礎(chǔ)上,增加了32-34行的隊(duì)列空檢查。需要注意的是,檢查必須放在隊(duì)列轉(zhuǎn)換的過程中,防止消費(fèi)者C2第一次嘗試時(shí)隊(duì)列非空,但第二次嘗試時(shí)隊(duì)列變空(由于C1取出了唯一的元素)的情況。

case2:隊(duì)列長(zhǎng)度等于1

隊(duì)列長(zhǎng)度等于1時(shí),入隊(duì)與出隊(duì)不會(huì)同時(shí)修改同一節(jié)點(diǎn),這時(shí)一定不會(huì)發(fā)生競(jìng)爭(zhēng)。分析如下。

假設(shè)存在一個(gè)生產(chǎn)者P1,一個(gè)消費(fèi)者C1,同時(shí)觸發(fā)入隊(duì)/出隊(duì),隊(duì)列中只有一個(gè)元素,所以只兩個(gè)節(jié)點(diǎn)dummyNode、singleNode則此時(shí):

assert dummy == dummyNode;
assert dummy.next.item == singleNode.item;

assert tail == singleNode;
assert tail.next == singleNode.next;

回顧C(jī)oncurrentLinkedQueue7的實(shí)現(xiàn):

  • poll()方法修改引用dummy、singleNode.item
  • offer()方法操tail、singleNode.next

因此,由于dummy node的引入,隊(duì)列長(zhǎng)度為1時(shí),入隊(duì)、出隊(duì)之間天生就不存在競(jìng)爭(zhēng)。

小結(jié)

至此,我們從最簡(jiǎn)單的場(chǎng)景觸發(fā),基于狀態(tài)機(jī)實(shí)現(xiàn)了一個(gè)支持高性能offer()、poll()方法的ConcurrentLinkedQueue7。CAS的好處暫且不表,重要的是基于狀態(tài)機(jī)進(jìn)行并發(fā)程序設(shè)計(jì)的思想。只有抓住其狀態(tài)機(jī)的本質(zhì),才能設(shè)計(jì)出正確、高效的并發(fā)類。

如果還是沒有體會(huì)到狀態(tài)機(jī)的精妙之處,可以拋開狀態(tài)機(jī),并自己嘗試基于樂觀策略實(shí)現(xiàn)ConcurrentLinkedQueue。(之所以要基于樂觀策略,是因?yàn)楸^策略可以認(rèn)為是樂觀策略的是特例,容易讓人忽略其狀態(tài)機(jī)的本質(zhì))

JDK實(shí)現(xiàn)

希望看到這里,你已經(jīng)理解了ConcurrentLinkedQueue的狀態(tài)機(jī)本質(zhì),因?yàn)橄旅婢筒辉偈潜疚牡闹攸c(diǎn)。

真·神Doug Lea的實(shí)現(xiàn)基于一個(gè)弱一致性的狀態(tài)機(jī):允許隊(duì)列處于多種不一致的狀態(tài),通過恰當(dāng)?shù)倪x擇“不一致的狀態(tài)”,能做到用戶無感;雖然增加了狀態(tài)機(jī)的復(fù)雜度,但也進(jìn)一步提高了性能。

網(wǎng)上分析文章非常多,讀者可自行閱讀,有一定難度。本文不打算講解Doug Lea的實(shí)現(xiàn),貼出源碼僅供大家膜拜

構(gòu)造方法

常用的是默認(rèn)的空構(gòu)造函數(shù):

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {
...
    private transient volatile Node<E> head;
    private transient volatile Node<E> tail;
    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }
...
}

Doug Lea也使用了dummy node,不過命名為了head。初始化方法同我們實(shí)現(xiàn)的ConcurrentLinkedQueue7。

入隊(duì)方法offer()

ConcurrentLinkedQueue7#offer()相當(dāng)于ConcurrentLinkedQueue#offer()的一個(gè)特例。

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) {
            if (p.casNext(null, newNode)) {
                if (p != t)
                    casTail(t, newNode);
                return true;
            }
        }
        else if (p == q)
            p = (t != (t = tail)) ? t : head;
        else
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

具體來講,ConcurrentLinkedQueue允許的多個(gè)狀態(tài)大體是這樣的:

  • 狀態(tài)S1:一致;newNode已銜接在tail.next,但tail指向倒數(shù)第1個(gè)節(jié)點(diǎn)
  • 狀態(tài)S2:不一致;newNode已銜接在tail.next,但tail指向倒數(shù)第2個(gè)節(jié)點(diǎn)
  • 狀態(tài)S3:不一致;newNode已銜接在tail.next,但tail指向倒數(shù)第3個(gè)節(jié)點(diǎn)
  • ...

狀態(tài)轉(zhuǎn)換的規(guī)則也隨之打破——不再需要連續(xù)完成T1、T2,可以連續(xù)執(zhí)行多次類T1,最后執(zhí)行一次類T2

for循環(huán)中的幾個(gè)分支就是在處理這些一致和不一致的狀態(tài)。我們前面定義的狀態(tài)機(jī)空間中只允許狀態(tài)S1、S2,因此是一個(gè)子集。增加的這些不一致的狀態(tài)主要是為了減少CAS次數(shù),進(jìn)一步提高隊(duì)列性能,這包含兩個(gè)重要意義:

  • 降低延遲:部分入隊(duì)請(qǐng)求不再需要走完完整的狀態(tài)轉(zhuǎn)換,只需要循環(huán)到tail.next.cas(null, newNode)成功。
  • 提高吞吐:之前每一次入隊(duì)請(qǐng)求都要設(shè)置一次tail節(jié)點(diǎn);目前只需要積攢幾次入隊(duì),在某個(gè)新的newNode入隊(duì)時(shí),直接嘗試tail.cas(t, newNode),將tail跳躍到最新的newNode。

增加這些不一致的狀態(tài)是很危險(xiǎn)的,如S3,當(dāng)隊(duì)列長(zhǎng)度為1的時(shí)候,tail與head的位置存在交叉。Doug Lea牛逼之處在于,在保證正確性的前提下,不僅通過增加狀態(tài)提高了性能,還減少了實(shí)際的CAS次數(shù)。

出隊(duì)方法poll()

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

            if (item != null && p.casItem(item, null)) {
                if (p != h)
                    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;
        }
    }
}
final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
        h.lazySetNext(h);
}

分析方法類似于offer()。注意下updateHead()。

未完

本來是想分析ConcurrentLinkedQueue源碼的,沒想到寫完?duì)顟B(tài)機(jī)就3600多字了,干貨卻不多。前路漫漫,源碼咱下回見。


本文鏈接:源碼|并發(fā)一枝花之ConcurrentLinkedQueue【偽】
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識(shí)共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布,歡迎轉(zhuǎn)載,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容