併發條件佇列之Condition 精講

雪中孤狼發表於2021-01-27

1. 條件佇列的意義

       Condition將Object監控器方法( wait , notify和notifyAll )分解為不同的物件,從而通過與任意Lock實現結合使用,從而使每個物件具有多個等待集。 Lock替換了synchronized方法和語句的使用,而Condition替換了Object監視器方法的使用。

       條件(也稱為條件佇列或條件變數)為一個執行緒暫停執行(“等待”)直到另一執行緒通知某些狀態條件現在可能為真提供了一種方法。 由於對該共享狀態資訊的訪問發生在不同的執行緒中,因此必須對其進行保護,因此某種形式的鎖與該條件相關聯。 等待條件提供的關鍵屬性是它自動釋放關聯的鎖並掛起當前執行緒,就像Object.wait一樣。

       Condition例項從本質上繫結到鎖。 要獲取特定Lock例項的Condition例項,請使用其newCondition()方法

2. 條件佇列原理

2.1 條件佇列結構

       條件佇列是一個單向連結串列,在該連結串列中我們使用nextWaiter屬性來串聯連結串列。但是,就像在同步佇列中不會使用nextWaiter屬性來串聯連結串列一樣,在條件佇列是中,也並不會用到prev, next屬性,它們的值都為null。

佇列的資訊包含以下幾個部分:

  1. private transient Node firstWaiter;// 佇列的頭部結點
  2. private transient Node lastWaiter;// 佇列的尾部節點

佇列中節點的資訊包含以下幾個部分:

  1. 當前節點的執行緒 thread
  2. 當前節點的狀態 waitStatus
  3. 當前節點的下一個節點指標 nextWaiter

結構圖:

在這裡插入圖片描述

注意:

       在條件佇列中,我們只需要關注一個值即可那就是CONDITION。它表示執行緒處於正常的等待狀態,而只要waitStatus不是CONDITION,我們就認為執行緒不再等待了,此時就要從條件佇列中出隊。

2.2 入隊原理

       每建立一個Condtion物件就會對應一個Condtion佇列,每一個呼叫了Condtion物件的await方法的執行緒都會被包裝成Node扔進一個條件佇列中

3. 條件佇列與同步佇列

       一般情況下,等待鎖的同步佇列和條件佇列條件佇列是相互獨立的,彼此之間並沒有任何關係。但是,當我們呼叫某個條件佇列的signal方法時,會將某個或所有等待在這個條件佇列中的執行緒喚醒,被喚醒的執行緒和普通執行緒一樣需要去爭鎖,如果沒有搶到,則同樣要被加到等待鎖的同步佇列中去,此時節點就從條件佇列中被轉移到同步佇列中

1. 條件佇列轉向同步佇列圖

圖片: https://uploader.shimo.im/f/HQ1slBQjxKVpHB0t.png
                            注意圖中標紅色的線

       但是,這裡尤其要注意的是,node是被 一個一個轉移過去的,哪怕我們呼叫的是signalAll()方法也是一個一個轉移過去的,而不是將整個條件佇列接在同步佇列的末尾。
       同時要注意的是,我們在同步佇列中只使用prev、next來串聯連結串列,而不使用nextWaiter;我們在條件佇列中只使用nextWaiter來串聯連結串列,而不使用prev、next.事實上,它們就是兩個使用了同樣的Node資料結構的完全獨立的兩種連結串列。
       因此,將節點從條件佇列中轉移到同步佇列中時,我們需要斷開原來的連結(nextWaiter),建立新的連結(prev, next),這某種程度上也是需要將節點一個一個地轉移過去的原因之一。

2. 條件佇列與同步佇列的區別

       同步佇列是等待鎖的佇列,當一個執行緒被包裝成Node加到該佇列中時,必然是沒有獲取到鎖;當處於該佇列中的節點獲取到了鎖,它將從該佇列中移除(事實上移除操作是將獲取到鎖的節點設為新的dummy head,並將thread屬性置為null)。

       條件佇列是等待在特定條件下的佇列,因為呼叫await方法時,必然是已經獲得了lock鎖,所以在進入條件佇列前執行緒必然是已經獲取了鎖;在被包裝成Node扔進條件佇列中後,執行緒將釋放鎖,然後掛起;當處於該佇列中的執行緒被signal方法喚醒後,由於佇列中的節點在之前掛起的時候已經釋放了鎖,所以必須先去再次的競爭鎖,因此,該節點會被新增到同步佇列中。因此,條件佇列在出隊時,執行緒並不持有鎖。

3. 條件佇列與同步佇列鎖關係

條件佇列:入隊時已經持有了鎖 -> 在佇列中釋放鎖 -> 離開佇列時沒有鎖 -> 轉移到同步佇列

同步佇列:入隊時沒有鎖 -> 在佇列中爭鎖 -> 離開佇列時獲得了鎖

4. 實戰用法

       例如,假設我們有一個有界緩衝區,它支援put和take方法。 如果嘗試在空緩衝區上進行take ,則執行緒將阻塞,直到有可用項為止。 如果嘗試在完整的緩衝區上進行put ,則執行緒將阻塞,直到有可用空間為止。 我們希望繼續等待put執行緒,並在單獨的等待集中take執行緒,以便我們可以使用僅當緩衝區中的專案或空間可用時才通知單個執行緒的優化。 這可以使用兩個Condition例項來實現一個典型的生產者-消費者模型。這裡在同一個lock鎖上,建立了兩個條件佇列fullCondition, notFullCondition。當佇列已滿,沒有儲存空間時,put方法在notFull條件上等待,直到佇列不是滿的;當佇列空了,沒有資料可讀時,take方法在notEmpty條件上等待,直到佇列不為空,而notEmpty.signal()和notFull.signal()則用來喚醒等待在這個條件上的執行緒。

public class BoundedQueue {
  /**
   * 生產者容器
   */
  private LinkedList<Object> buffer;
  /**
   * 容器最大值是多少
   */
  private int maxSize;
  /**
   * 鎖
   */
  private Lock lock;
  /**
   * 滿了
   */
  private Condition fullCondition;
  /**
   * 不滿
   */
  private Condition notFullCondition;
  BoundedQueue(int maxSize) {
    this.maxSize = maxSize;
    buffer = new LinkedList<Object>();
    lock = new ReentrantLock();
    fullCondition = lock.newCondition();
    notFullCondition = lock.newCondition();
  }
  /**
   * 生產者
   *
   * @param obj
   * @throws InterruptedException
   */
  public void put(Object obj) throws InterruptedException {
    //獲取鎖
    lock.lock();
    try {
      while (maxSize == buffer.size()) {
        System.out.println(Thread.currentThread().getName() + "此時佇列滿了,新增的執行緒進入等待狀態");
        // 佇列滿了,新增的執行緒進入等待狀態
        notFullCondition.await();
      }
      buffer.add(obj);
      //通知
      fullCondition.signal();
    } finally {
      lock.unlock();
    }
  }
  /**
   * 消費者
   *
   * @return
   * @throws InterruptedException
   */
  public Object take() throws InterruptedException {
    Object obj;
    lock.lock();
    try {
      while (buffer.size() == 0) {
        System.out.println(Thread.currentThread().getName() + "此時佇列空了執行緒進入等待狀態");
        // 佇列空了執行緒進入等待狀態
        fullCondition.await();
      }
      obj = buffer.poll();
      //通知
      notFullCondition.signal();
    } finally {
      lock.unlock();
    }
    return obj;
  }
  public static void main(String[] args) {
    // 初始化最大能放2個元素的佇列
    BoundedQueue boundedQueue = new BoundedQueue(2);
    for (int i = 0; i < 3; i++) {
      Thread thread = new Thread(() -> {
        try {
          boundedQueue.put("元素");
          System.out.println(Thread.currentThread().getName() + "生產了元素");
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      });
      thread.setName("執行緒" + i);
      thread.start();
    }
    for (int i = 0; i < 3; i++) {
      Thread thread = new Thread(() -> {
        try {
          boundedQueue.take();
          System.out.println(Thread.currentThread().getName() + "消費了元素");
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      });
      thread.setName("執行緒" + i);
      thread.start();
    }
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

輸出結果:
圖片: https://uploader.shimo.im/f/ZbYdtEOSIvSGoInd.png

5. 原始碼分析

Condition介面中的方法

1. await()

       實現可中斷條件等待,其實我們以上案例是利用ReentrantLock來實現的生產者消費者案例,進去看原始碼發現其實實現該方法的是 AbstractQueuedSynchronizer 中ConditionObject實現的
       將節點新增進同步佇列中,並要麼立即喚醒執行緒,要麼等待前驅節點釋放鎖後將自己喚醒,無論怎樣,被喚醒的執行緒要從哪裡恢復執行呢?呼叫了await方法的地方

中斷模式interruptMode這個變數記錄中斷事件,該變數有三個值:

  1. 0 : 代表整個過程中一直沒有中斷髮生。
  2. THROW_IE : 表示退出await()方法時需要丟擲InterruptedException,這種模式對應於中斷髮生在signal之前
  3. REINTERRUPT : 表示退出await()方法時只需要再自我中斷以下,這種模式對應於中斷髮生在signal之後。
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 新增節點到條件佇列中
    Node node = addConditionWaiter();
     // 釋放當前執行緒所佔用的鎖,儲存當前的鎖狀態
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 如果當前佇列不在同步佇列中,說明剛剛被await, 還沒有人呼叫signal方法,
    // 則直接將當前執行緒掛起
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this); // 執行緒掛起的地方
         // 執行緒將在這裡被掛起,停止執行
        // 能執行到這裡說明要麼是signal方法被呼叫了,要麼是執行緒被中斷了
        // 所以檢查下執行緒被喚醒的原因,如果是因為中斷被喚醒,則跳出while迴圈
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
   // 執行緒將在同步佇列中利用進行acquireQueued方法進行“阻塞式”爭鎖,
   // 搶到鎖就返回,搶不到鎖就繼續被掛起。因此,當await()方法返回時,
   // 必然是保證了當前執行緒已經持有了lock鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

addConditionWaiter() 方法是封裝一個節點將該節點放入條件佇列中

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    // 如果尾節點被cancel了,則先遍歷整個連結串列,清除所有被cancel的節點
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 將當前執行緒包裝成Node扔進條件佇列
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 如果當前節點為空值那麼新建立的node節點就是第一個等待節點
    if (t == null)
        firstWaiter = node;
    // 如果當前節點不為空值那麼新建立的node節點就加入到當前節點的尾部節點的下一個
    else
        t.nextWaiter = node;
    lastWaiter = node; // 尾部節點指向當前節點
    return node; // 返回新加入的節點
}

注意:

  1. 節點加入條件佇列時waitStatus的值為Node.CONDTION。
  2. 如果入隊時發現尾節點已經取消等待了,那麼我們就不應該接在它的後面,此時需要呼叫unlinkCancelledWaiters來清除那些已經取消等待的執行緒(條件佇列從頭部進行遍歷的,同步佇列是從尾部開始遍歷的)
private void unlinkCancelledWaiters() {
    // 獲取佇列的頭節點
    Node t = firstWaiter;
    Node trail = null;
    // 當前節點不為空 
    while (t != null) {
       // 獲取下一個節點
        Node next = t.nextWaiter;
        // 如果當前節點不是條件節點
        if (t.waitStatus != Node.CONDITION) {
            // 在佇列中取消當前節點
            t.nextWaiter = null;
            if (trail == null)
                // 佇列的頭節點是當前節點的下一個節點
                firstWaiter = next;
            else
                // trail的 nextWaiter 指向當前節點t的下一個節點
                // 因為此時t節點已經被取消了
                trail.nextWaiter = next;
                // 如果t節點的下一個節點為空那麼lastWaiter指向trail
            if (next == null)
                lastWaiter = trail;
        }
        else
            // 如果是條件節點 trail 指向當前節點
            trail = t;
        // 迴圈賦值遍歷
        t = next;
    }
}

fullyRelease(node) 方法釋放當前執行緒所佔用的鎖

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        // 如果釋放成功
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            // 節點的狀態被設定成取消狀態,從同步佇列中移除
            node.waitStatus = Node.CANCELLED;
    }
}
public final boolean release(int arg) {
    // 嘗試獲取鎖,如果獲取成功,喚醒後續執行緒
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

執行緒喚醒後利用checkInterruptWhileWaiting方法檢測中斷模式

  1. 情況一中斷髮生時,執行緒還沒有被喚醒過

       這裡假設已經發生過中斷,則Thread.interrupted()方法必然返回true,接下來就是用transferAfterCancelledWait進一步判斷是否發生了signal。

 // 檢查是否有中斷,如果在發出訊號之前被中斷,則返回THROW_IE;
 // 在發出訊號之後,則返回REINTERRUPT;如果沒有被中斷,則返回0。
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}
final boolean transferAfterCancelledWait(Node node) {
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

       只要一個節點的waitStatus還是Node.CONDITION,那就說明它還沒有被signal過。
       由於現在我們分析情況1,則當前節點的waitStatus必然是Node.CONDITION,則會成功執行compareAndSetWaitStatus(node, Node.CONDITION, 0),將該節點的狀態設定成0,然後呼叫enq(node)方法將當前節點新增進同步佇列中,然後返回true。

注意: 我們此時並沒有斷開node的nextWaiter,所以最後一定不要忘記將這個連結斷開。
       再回到transferAfterCancelledWait呼叫處,可知,由於transferAfterCancelledWait將返回true,現在checkInterruptWhileWaiting將返回THROW_IE,這表示我們在離開await方法時應當要丟擲THROW_IE異常。

   // ....
   while (!isOnSyncQueue(node)) {
        LockSupport.park(this); // 執行緒掛起的地方
         // 執行緒將在這裡被掛起,停止執行
        // 能執行到這裡說明要麼是signal方法被呼叫了,要麼是執行緒被中斷了
        // 所以檢查下執行緒被喚醒的原因,如果是因為中斷被喚醒,則跳出while迴圈
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
   // 執行緒將在同步佇列中利用進行acquireQueued方法進行“阻塞式”爭鎖,
   // 搶到鎖就返回,搶不到鎖就繼續被掛起。因此,當await()方法返回時,
   // 必然是保證了當前執行緒已經持有了lock鎖
   
   // 我們這裡假設它獲取到了鎖了,由於我們這時
   // 的interruptMode = THROW_IE,則會跳過if語句。
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 遍歷連結串列了,把連結串列中所有沒有在等待的節點都拿出去,所以這裡呼叫
    // unlinkCancelledWaiters方法,該方法我們在前面await()第一部分的分析
    // 的時候已經講過了,它就是簡單的遍歷連結串列,找到所有waitStatus
    // 不為CONDITION的節點,並把它們從佇列中移除
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 這裡我們的interruptMode=THROW_IE,說明發生了中斷,
    // 則將呼叫reportInterruptAfterWait
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    }
 }
// 在interruptMode=THROW_IE時,就是簡單的丟擲了一個InterruptedException
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

       interruptMode現在為THROW_IE,則我們將執行break,跳出while迴圈。接下來我們將執行acquireQueued(node, savedState)進行爭鎖,注意,這裡傳入的需要獲取鎖的重入數量是savedState,即之前釋放了多少,這裡就需要再次獲取多少
情況一總結:

  1. 執行緒因為中斷,從掛起的地方被喚醒
  2. 隨後,我們通過transferAfterCancelledWait確認了執行緒的waitStatus值為Node.CONDITION,說明並沒有signal發生過
  3. 然後我們修改執行緒的waitStatus為0,並通過enq(node)方法將其新增到同步佇列中
  4. 接下來執行緒將在同步佇列中以阻塞的方式獲取,如果獲取不到鎖,將會被再次掛起
  5. 執行緒在同步佇列中獲取到鎖後,將呼叫unlinkCancelledWaiters方法將自己從條件佇列中移除,該方法還會順便移除其他取消等待的鎖
  6. 最後我們通過reportInterruptAfterWait丟擲了InterruptedException

因此:

       由此可以看出,一個呼叫了await方法掛起的執行緒在被中斷後不會立即丟擲InterruptedException,而是會被新增到同步佇列中去爭鎖,如果爭不到,還是會被掛起;

       只有爭到了鎖之後,該執行緒才得以從同步佇列和條件佇列中移除,最後丟擲InterruptedException。

       所以說,一個呼叫了await方法的執行緒,即使被中斷了,它依舊還是會被阻塞住,直到它獲取到鎖之後才能返回,並在返回時丟擲InterruptedException。中斷對它意義更多的是體現在將它從條件佇列中移除,加入到同步佇列中去爭鎖,從這個層面上看,中斷和signal的效果其實很像,所不同的是,在await()方法返回後,如果是因為中斷被喚醒,則await()方法需要丟擲InterruptedException異常,表示是它是被非正常喚醒的(正常喚醒是指被signal喚醒)

  1. 情況二中斷髮生時,執行緒已經被喚醒過包含以下兩種情況
    a. 被喚醒時,已經發生了中斷,但此時執行緒已經被signal過了
final boolean transferAfterCancelledWait(Node node) {
// 執行緒A執行到這裡,CAS操作將會失敗
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { 
        enq(node);
        return true;
    }
// 由於中斷髮生前,執行緒已經被signal了,則這裡只需要等待執行緒成功進入同步即可
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

       由於transferAfterCancelledWait返回了false,則checkInterruptWhileWaiting方法將返回REINTERRUPT,這說明我們在退出該方法時只需要再次中斷因為signal後條件佇列加入到了同步佇列中所以node.nextWaiter為空了,所以直接走到了reportInterruptAfterWait(interruptMode)方法

    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 這裡我們的interruptMode=THROW_IE,說明發生了中斷,
    // 則將呼叫reportInterruptAfterWait
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    }
 }
// 在interruptMode=THROW_IE時,就是簡單的丟擲了一個InterruptedException
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
   // 這裡並沒有丟擲中斷異常,而只是將當前執行緒再中斷一次。
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

情況二中的第一種情況總結:

  1. 執行緒從掛起的地方被喚醒,此時既發生過中斷,又發生過signal

  2. 隨後,我們通過transferAfterCancelledWait確認了執行緒的waitStatus值已經不為Node.CONDITION,說明signal發生於中斷之前

  3. 然後,我們通過自旋的方式,等待signal方法執行完成,確保當前節點已經被成功新增到同步佇列中

  4. 接下來執行緒將在同步佇列中以阻塞的方式獲取鎖,如果獲取不到,將會被再次掛起

  5. 最後我們通過reportInterruptAfterWait將當前執行緒再次中斷,但是不會丟擲InterruptedException

    b. 被喚醒時,並沒有發生中斷,但是在搶鎖的過程中發生了中斷

       此情況就是已經被喚醒了那麼isOnSyncQueue(node)返回true,在同步佇列中了就,退出了while迴圈。
       退出while迴圈後接下來還是利用acquireQueued爭鎖,因為前面沒有發生中斷,則interruptMode=0,這時,如果在爭鎖的過程中發生了中斷,則acquireQueued將返回true,則此時interruptMode將變為REINTERRUPT。
       接下是判斷node.nextWaiter != null,由於在呼叫signal方法時已經將節點移出了佇列,所有這個條件也不成立。
       最後就是彙報中斷狀態了,此時interruptMode的值為REINTERRUPT,說明執行緒在被signal後又發生了中斷,這個中斷髮生在搶鎖的過程中,這個中斷來的太晚了,因此我們只是再次自我中斷一下。

情況二中的第二種情況總結:

  1. 執行緒被signal方法喚醒,此時並沒有發生過中斷
  2. 因為沒有發生過中斷,我們將從checkInterruptWhileWaiting處返回,此時interruptMode=0
  3. 接下來我們回到while迴圈中,因為signal方法保證了將節點新增到同步佇列中,此時while迴圈條件不成立,迴圈退出
  4. 接下來執行緒將在同步佇列中以阻塞的方式獲取,如果獲取不到鎖,將會被再次掛起
  5. 執行緒獲取到鎖返回後,我們檢測到在獲取鎖的過程中發生過中斷,並且此時interruptMode=0,這時,我們將interruptMode修改為REINTERRUPT
  6. 最後我們通過reportInterruptAfterWait將當前執行緒再次中斷,但是不會丟擲InterruptedException

3.情況三一直沒發生中斷
       直接正常返回

await方法總結

  1. 進入await()時必須是已經持有了鎖
  2. 離開await()時同樣必須是已經持有了鎖
  3. 呼叫await()會使得當前執行緒被封裝成Node扔進條件佇列,然後釋放所持有的鎖
  4. 釋放鎖後,當前執行緒將在條件佇列中被掛起,等待signal或者中斷
  5. 執行緒被喚醒後會將會離開條件佇列進入同步佇列中進行搶鎖
  6. 若線上程搶到鎖之前發生過中斷,則根據中斷髮生在signal之前還是之後記錄中斷模式
  7. 執行緒在搶到鎖後進行善後工作(離開條件佇列,處理中斷異常)
  8. 執行緒已經持有了鎖,從await()方法返回
    在這裡插入圖片描述
           在這一過程中我們尤其要關注中斷,如前面所說,中斷和signal所起到的作用都是將執行緒從條件佇列中移除,加入到同步佇列中去爭鎖,所不同的是,signal方法被認為是正常喚醒執行緒,中斷方法被認為是非正常喚醒執行緒,如果中斷髮生在signal之前,則我們在最終返回時,應當丟擲InterruptedException;如果中斷髮生在signal之後,我們就認為執行緒本身已經被正常喚醒了,這個中斷來的太晚了,我們直接忽略它,並在await()返回時再自我中斷一下,這種做法相當於將中斷推遲至await()返回時再發生。

2. awaitUninterruptibly

public final void awaitUninterruptibly() {
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean interrupted = false;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if (Thread.interrupted())
       //  發生了中斷後執行緒依舊留在了條件佇列中,將會再次被掛起
            interrupted = true;
    }
    if (acquireQueued(node, savedState) || interrupted)
        selfInterrupt();
}

       由此可見,awaitUninterruptibly()全程忽略中斷,即使是當前執行緒因為中斷被喚醒,該方法也只是簡單的記錄中斷狀態,然後再次被掛起(因為並沒有並沒有任何操作將它新增到同步佇列中)要使當前執行緒離開條件佇列去爭鎖,則必須是發生了signal事件。
       最後,當執行緒在獲取鎖的過程中發生了中斷,該方法也是不響應,只是在最終獲取到鎖返回時,再自我中斷一下。可以看出,該方法和“中斷髮生於signal之後的”REINTERRUPT模式的await()方法很像

方法總結

  1. 中斷雖然會喚醒執行緒,但是不會導致執行緒離開條件佇列,如果執行緒只是因為中斷而被喚醒,則他將再次被掛起
  2. 只有signal方法會使得執行緒離開條件佇列
  3. 呼叫該方法時或者呼叫過程中如果發生了中斷,僅僅會在該方法結束時再自我中斷以下,不會丟擲InterruptedException

3. awaitNanos

       該方法幾乎和await()方法一樣,只是多了超時時間的處理該方法的主要設計思想是,如果設定的超時時間還沒到,我們就將執行緒掛起;超過等待的時間了,我們就將執行緒從條件佇列轉移到同步對列中。

public final long awaitNanos(long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    final long deadline = System.nanoTime() + nanosTimeout;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        if (nanosTimeout <= 0L) {
            transferAfterCancelledWait(node);
            break;
        }
        if (nanosTimeout >= spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanosTimeout);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        nanosTimeout = deadline - System.nanoTime();
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return deadline - System.nanoTime();
}

4. await(long time, TimeUnit unit)

       在awaitNanos(long nanosTimeout)的基礎上多了對於超時時間的時間單位的設定,但是在內部實現上還是會把時間轉成納秒去執行。
       可以看出,這兩個方法主要的差別就體現在返回值上面,awaitNanos(long nanosTimeout)的返回值是剩餘的超時時間,如果該值大於0,說明超時時間還沒到,則說明該返回是由signal行為導致的,而await(long time, TimeUnit unit)的返回值就是transferAfterCancelledWait(node)的值,我們知道,如果呼叫該方法時,node還沒有被signal過則返回true,node已經被signal過了,則返回false。因此當await(long time, TimeUnit unit)方法返回true,則說明在超時時間到之前就已經發生過signal了,該方法的返回是由signal方法導致的而不是超時時間。

public final boolean await(long time, TimeUnit unit)
        throws InterruptedException {
    long nanosTimeout = unit.toNanos(time);
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    final long deadline = System.nanoTime() + nanosTimeout;
    boolean timedout = false;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        if (nanosTimeout <= 0L) {
            timedout = transferAfterCancelledWait(node);
            break;
        }
        if (nanosTimeout >= spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanosTimeout);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        nanosTimeout = deadline - System.nanoTime();
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return !timedout;
}

5. awaitUntil

       awaitUntil(Date deadline)方法與上面的幾種帶超時的方法也基本類似,所不同的是它的超時時間是一個絕對的時間

public final boolean awaitUntil(Date deadline)
        throws InterruptedException {
    long abstime = deadline.getTime();
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean timedout = false;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        if (System.currentTimeMillis() > abstime) {
            timedout = transferAfterCancelledWait(node);
            break;
        }
        LockSupport.parkUntil(this, abstime);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return !timedout;
}

6. signal

只喚醒一個節點

public final void signal() {
 // getExclusiveOwnerThread() == Thread.currentThread(); 當前線
 // 程是不是獨佔執行緒
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 獲取第一個阻塞執行緒節點
    Node first = firstWaiter;
    // 條件佇列是否為空
    if (first != null)
        doSignal(first);
}
// 遍歷整個條件佇列,找到第一個沒有被cancelled的節點,並將它新增到條件佇列的末尾
// 如果條件佇列裡面已經沒有節點了,則將條件佇列清空
private void doSignal(Node first) {
    do {
        // 將firstWaiter指向條件佇列隊頭的下一個節點
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // 將條件佇列原來的隊頭從條件佇列中斷開,則此時該節點成為一個孤立的節點
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

方法總結:
       呼叫signal()方法會從當前條件佇列中取出第一個沒有被cancel的節點新增到sync佇列的末尾。

7. signalAll

喚醒所有的節點

public final void signalAll() {
 // getExclusiveOwnerThread() == Thread.currentThread(); 當前線
 // 程是不是獨佔執行緒
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 獲取第一個阻塞執行緒節點
    Node first = firstWaiter;
   // 條件佇列是否為空
    if (first != null)
        doSignalAll(first);
}
// 移除並轉移所有節點
private void doSignalAll(Node first) {
    // 清空佇列中所有資料
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}
// 將條件佇列中的節點一個一個的遍歷到同步佇列中
final boolean transferForSignal(Node node) {
  // 如果該節點在呼叫signal方法前已經被取消了,則直接跳過這個節點
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
    return false;
 // 利用enq方法將該節點新增至同步佇列的尾部   
    Node p = enq(node); 
    // 返回的是前驅節點,將其設定SIGNAL之後,才會掛起
    // 當前節點
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

       在transferForSignal方法中,我們先使用CAS操作將當前節點的waitStatus狀態由CONDTION設為0,如果修改不成功,則說明該節點已經被CANCEL了則我們直接返回操作下一個節點;如果修改成功,則說明我們已經將該節點從等待的條件佇列中成功“喚醒”了,但此時該節點對應的執行緒並沒有真正被喚醒,它還要和其他普通執行緒一樣去爭鎖,因此它將被新增到同步佇列的末尾等待獲取鎖 。
方法總結:

  1. 將條件佇列清空(只是令lastWaiter = firstWaiter = null,佇列中的節點和連線關係仍然還存在)
  2. 將條件佇列中的頭節點取出,使之成為孤立節點(nextWaiter,prev,next屬性都為null)
  3. 如果該節點處於被Cancelled了的狀態,則直接跳過該節點(由於是孤立節點,則會被GC回收)
  4. 如果該節點處於正常狀態,則通過enq方法將它新增到同步佇列的末尾
  5. 判斷是否需要將該節點喚醒(包括設定該節點的前驅節點的狀態為SIGNAL),如有必要,直接喚醒該節點
  6. 重複2-5,直到整個條件佇列中的節點都被處理完

6. 總結

       以上便是Condition的分析,下一篇文章將是併發容器類的分析,如有錯誤之處,幫忙指出及時更正,謝謝, 如果喜歡謝謝點贊加收藏加轉發(轉發註明出處謝謝!!!)

相關文章