17.AQS中的Condition是什麼?

王有志發表於2023-05-17

歡迎關注:王有志
期待你加入Java人的提桶跑路群:共同富裕的Java人

今天來和大家聊聊ConditionCondition為AQS“家族”提供了等待與喚醒的能力,使AQS"家族"具備了像synchronized一樣暫停與喚醒執行緒的能力。我們先來看兩道關於Condition的面試題目:

  • ConditionObject的等待與喚醒有什麼區別?
  • 什麼是Condition佇列?

接下來,我們就按照“是什麼”,“怎麼用”和“如何實現”的順序來揭開Condition的面紗吧。

Condition是什麼?

Condition是Java中的介面,提供了與Object#waitObject#notify相同的功能。Doug Lea在Condition介面的描述中提到了這點:

Conditions (also known as condition queues or condition variables) provide a means for one thread to suspend execution (to "wait") until notified by another thread that some state condition may now be true.

來看Condition介面中提供了哪些方法:

public interface Condition {
  void await() throws InterruptedException;
  
  void awaitUninterruptibly();
  
  long awaitNanos(long nanosTimeout) throws InterruptedException;
  
  boolean await(long time, TimeUnit unit) throws InterruptedException;
  
  boolean awaitUntil(Date deadline) throws InterruptedException;
  
  void signal();
  
  void signalAll();
}

Condition只提供了兩個功能:等待(await)和喚醒(signal),與Object提供的等待與喚醒時相似的:

public final void wait() throws InterruptedException;
  
public final void wait(long timeoutMillis, int nanos) throws InterruptedException;

public final native void wait(long timeoutMillis) throws InterruptedException;

@HotSpotIntrinsicCandidate
public final native void notify();

@HotSpotIntrinsicCandidate
public final native void notifyAll();

喚醒功能上,ConditionObject的差異並不大:

  • Condition#signal\(\approx\)Object#notify

  • Condition#signalAll\(=\)Object#notifyAll

多個執行緒處於等待狀態時,Object#notify()是“隨機”喚醒執行緒,而Condition#signal則由具體實現決定如何喚醒執行緒,如:ConditionObject喚醒的是最早進入等待的執行緒但兩個方法均只喚醒一個執行緒。

等待功能上,ConditionObject的共同點是:都會釋放持有的資源Condition釋放鎖Object釋放Monitor,即進入等待狀態後允許其他執行緒獲取鎖/監視器。主要的差異體現在Condition支援了更加豐富的場景,透過一張表格來對比下:

Condition方法 Object方法 解釋
Condition#await() Object#wait() 暫停執行緒,丟擲執行緒中斷異常
Condition#awaitUninterruptibly() / 暫停執行緒,不丟擲執行緒中斷異常
Condition#await(time, unit) Object#wait(timeoutMillis, nanos) 暫停執行緒,直到被喚醒或等待指定時間後,超時後自動喚醒返回false,否則返回true
Condition#awaitUntil(deadline) / 暫停執行緒,直到被喚醒或到達指定時間點,超時後自動喚醒返回false,否則返回true
Condition#awaitNanos(nanosTimeout) / 暫停執行緒,直到被喚醒或等待指定時間後,返回值表示被喚醒時的剩餘時間(nanosTimeout-耗時),結果為負數表示超時

除了以上差異外,Condition還支援建立多個等待佇列,即同一把鎖擁有多個等待佇列,執行緒在不同佇列中等待,而Object只有一個等待佇列。《Java併發程式設計的藝術》中也有一張類似的表格,放在這裡供大家參考:

Tips

  • 實際上signal翻譯為喚醒並不恰當~~
  • 涉及到Condition的實現部分,下文透過AQS中的ConditionObject詳細解釋。

Condition怎麼用?

既然ConditionObject提供的等待與喚醒功能相同,那麼它們的用法是不是也很相似呢?

與呼叫Object#waitObject#notifyAll必須處於synchronized修飾的程式碼中一樣(獲取Monitor),呼叫Condition#awaitCondition#signalAll的前提是要先獲取鎖。但不同的是,使用Condition前,需要先透過鎖去建立Condition

ReentrantLock中提供的Condition為例,首先是建立Condition物件:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

然後是獲取鎖並呼叫await方法:

new Thread(() -> {
  lock.lock();
  try {
    condition.await();
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }
  lock.unlock();
}

最後,透過呼叫singalAll喚醒全部阻塞中的執行緒:

new Thread(() -> {
  lock.lock();
  condition.signalAll();
  lock.unlock();
}

ConditionObject的原始碼分析

作為介面Condition非常慘,因為在Java中只有AQS中的內部類ConditionObject實現了Condition介面:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  
  public class ConditionObject implements Condition, java.io.Serializable {
    private transient Node firstWaiter;
    
    private transient Node lastWaiter;
  }
  
  static final class Node {
    // 省略
  }
}

ConditionObject只有兩個Node型別的欄位,分別是鏈式結構中的頭尾節點,ConditionObject就是透過它們實現的等待佇列。那麼ConditionObject的等待佇列起到了怎樣的作用呢?是類似於AQS中的排隊機制嗎?帶著這兩個問題,我們正是開始原始碼的分析。

await方法的實現

Condition介面中定義了4個執行緒等待的方法:

  • void await() throws InterruptedException
  • void awaitUninterruptibly();
  • long awaitNanos(long nanosTimeout) throws InterruptedException;
  • boolean await(long time, TimeUnit unit) throws InterruptedException;
  • boolean awaitUntil(Date deadline) throws InterruptedException;

方法雖然很多,但它們之間的差異較小,只體現在時間的處理上,我們看其中最常用的方法:

public final void await() throws InterruptedException {
  // 執行緒中斷,丟擲異常
  if (Thread.interrupted()) {
    throw new InterruptedException();
  }
  // 註釋1:加入到Condition的等待佇列中
  Node node = addConditionWaiter();
  // 註釋2:釋放持有鎖(呼叫AQS的release)
  int savedState = fullyRelease(node);
  int interruptMode = 0;
  // 註釋3:判斷是否在AQS的等待佇列中
  while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
    // 中斷時退出方法
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
      break;
    }
  }
  
  // 加入到AQS的等待佇列中,呼叫AQS的acquireQueued方法
  if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
    interruptMode = REINTERRUPT;
  }
  
  // 斷開與Condition佇列的聯絡
  if (node.nextWaiter != null) {
    unlinkCancelledWaiters();
  }
  
  if (interruptMode != 0) {
   reportInterruptAfterWait(interruptMode);
  }
}

註釋1的部分,呼叫addConditionWaiter方法新增到Condition佇列中:

private Node addConditionWaiter() {
  // 判斷當前執行緒是否為持有鎖的執行緒
  if (!isHeldExclusively()) {
    throw new IllegalMonitorStateException();
  }
  
  // 獲取Condition佇列的尾節點
  Node t = lastWaiter;
  // 斷開不再位於Condition佇列的節點
  if (t != null && t.waitStatus != Node.CONDITION) {
    unlinkCancelledWaiters();
    t = lastWaiter;
  }
  
  // 建立Node.CONDITION模式的Node節點
  Node node = new Node(Node.CONDITION);
  if (t == null) {
    // 佇列為空的場景,將node設定為頭節點
    firstWaiter = node;
  } else {
    // 佇列不為空的場景,將node新增到尾節點的後繼節點上
    t.nextWaiter = node;
  }
  // 更新尾節點
  lastWaiter = node;
  return node;
}

可以看到,Condition的佇列是一個樸實無華的雙向連結串列,每次呼叫addConditionWaiter方法,都會加入到Condition佇列的尾部。

註釋2的部分,釋放執行緒持有的鎖,同時移出AQS的佇列,內部呼叫了AQS的release方法:

=final int fullyRelease(Node node) {
  try {
    int savedState = getState();
    if (release(savedState)) {
      return savedState;
    }
    throw new IllegalMonitorStateException();
  } catch (Throwable t) {
    node.waitStatus = Node.CANCELLED;
    throw t;
  }
}

因為已經分析過AQS的release方法和ReentrantLock實現的tryRelease方法,這裡我們就不過多贅述了。

註釋3的部分,isOnSyncQueue判斷當前執行緒是否在AQS的等待佇列中,我們來看此時存在的情況:

  • 如果isOnSyncQueue返回false,即執行緒不在AQS的佇列中,進入自旋,呼叫LockSupport#park暫停執行緒;
  • 如果isOnSyncQueue返回true,即執行緒在AQS的佇列中,不進入自旋,執行後續邏輯。

結合註釋1和註釋2的部分,Condition#await的實現原理了就很清晰了:

  • Condition與AQS分別維護了一個等待佇列,而且是互斥的,即同一個節點只會出現在一個佇列中
  • 當呼叫Condition#await時,將執行緒新增到Condition的佇列中(註釋1),同時從AQS佇列中移出(註釋2);
  • 接著判斷執行緒位於的佇列:
    • 位於Condition佇列中,該執行緒需要被暫停,呼叫LockSupport#park
    • 位於AQS佇列中,該執行緒正在等待獲取鎖。

基於以上的結論,我們已經能夠猜到喚醒方法Condition#signalAll的原理了:

  • 將執行緒從Condition佇列中移出,並新增到AQS的佇列中;
  • 呼叫LockSupport.unpark喚醒執行緒。

至於這個猜想是否正確,我們接著來看喚醒方法的實現。

Tips:如果忘記了AQS中相關方法是如何實現的,可以回顧下《AQS的今生,構建出JUC的基礎》。

signal和signalAll方法的實現

來看signalsignalAll的原始碼:

// 喚醒一個處於等待中的執行緒
public final void signal() {
  if (!isHeldExclusively()) {
    throw new IllegalMonitorStateException();
  }
  // 獲取Condition佇列中的第一個節點
  Node first = firstWaiter;
  if (first != null) {
    // 喚醒第一個節點
    doSignal(first);
  }
}

// 喚醒全部處於等待中的執行緒
public final void signalAll() {
    if (!isHeldExclusively()){
      throw new IllegalMonitorStateException();
    }
        
    Node first = firstWaiter;
    if (first != null) {
      // 喚醒所有節點
      doSignalAll(first);
    }  
}

兩個方法唯一的差別在於頭節點不為空的場景下,是呼叫doSignal喚醒一個執行緒還是呼叫doSignalAll喚醒所有執行緒:

private void doSignal(Node first) {
  do {
    // 更新頭節點
    if ( (firstWaiter = first.nextWaiter) == null) {
      // 無後繼節點的場景
      lastWaiter = null;
    }
    // 斷開節點的連線
    first.nextWaiter = null;
    // 喚醒頭節點
  } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

private void doSignalAll(Node first) {
  // 將Condition的佇列置為空
  lastWaiter = firstWaiter = null;
  do {
    // 斷開連結
    Node next = first.nextWaiter;
    first.nextWaiter = null;
    // 喚醒當前頭節點
    transferForSignal(first);
    // 更新頭節點
    first = next;
  } while (first != null);
}

可以看到,無論是doSignal還是doSignalAll都只是將節點移出Condition佇列,而真正起到喚醒作用的是transferForSignal方法,從方法名可以看到該方法是透過“轉移”進行喚醒的,我們來看原始碼:

final boolean transferForSignal(Node node) {
  // 透過CAS替換node的狀態
  // 如果替換失敗,說明node不處於Node.CONDITION狀態,不需要喚醒
  if (!node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
    return false;
  }
  // 將節點新增到AQS的佇列的隊尾
  // 並返回老隊尾節點,即node的前驅節點
  Node p = enq(node);
  int ws = p.waitStatus;
  // 對前驅節點狀態的判斷
  if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL)) {
    LockSupport.unpark(node.thread);
  }
  return true;
}

transferForSignal方法中,呼叫enq方法將node重新新增到AQS的佇列中,並返回node的前驅節點,隨後對前驅節點的狀態進行判斷:

  • \(ws > 0\)時,前驅節點處於Node.CANCELLED狀態,前驅節點退出鎖的爭搶,node可以直接被喚醒;
  • \(ws \leq 0\)時,透過CAS修改前驅節點的狀態為Node.SIGNAL,設定失敗時,直接喚醒node

AQS的今生,構建出JUC的基礎》中介紹了waitStatus的5種狀態,其中Node.SIGNAL狀態表示需要喚醒後繼節點。另外,在分析shouldParkAfterFailedAcquire方法的原始碼時,我們知道在進入AQS的等待佇列時,需要將前驅節點的狀態更新為Node.SIGNAL

最後來看enq的實現:

private Node enq(Node node) {
  for (;;) {
    // 獲取尾節點
    Node oldTail = tail;
    if (oldTail != null) {
      // 更新當前節點的前驅節點
      node.setPrevRelaxed(oldTail);
      // 更新尾節點
      if (compareAndSetTail(oldTail, node)) {
        oldTail.next = node;
        // 返回當前節點的前驅節點(即老尾節點)
        return oldTail;
      }
    } else {
      initializeSyncQueue();
    }
  }
}

enq的實現就非常簡單了,透過CAS更新AQS的佇列尾節點,相當於新增到AQS的佇列中,並返回尾節點的前驅節點。好了,喚醒方法的原始碼到這裡就結束了,是不是和我們當初的猜想一模一樣呢?

圖解ConditionObject原理

功能上,Condition實現了AQS版Object#waitObject#notify,用法上也與之相似,需要先獲取鎖,即需要在lockunlock之間呼叫。原理上,簡單來說就是執行緒在AQS的佇列和Condition的佇列之間的轉移

執行緒t持有鎖

假設有執行緒t已經獲取了ReentrantLock,執行緒t1,t2和t3正在AQS的佇列中等待,我們可以得到這樣的結構:

執行緒t執行Condition#await

如果執行緒t中呼叫了Condition#await方法,執行緒t進入Condition的等待佇列中,執行緒t1獲取ReentrantLock,並從AQS的佇列中移出,結構如下:

執行緒t1執行Condition#await

如果執行緒t1中也執行了Condition#await方法,同樣執行緒t1進入Condition佇列中,執行緒t2獲取到ReentrantLock,結構如下:

執行緒t2執行Condition#signal

如果執行緒t2執行了Condition#signal,喚醒Condition佇列中的第一個執行緒,此時結構如下:

透過上面的流程,我們就可以得到執行緒是如何在Condition佇列與AQS佇列中轉移的:

結語

關於Condition的內容到這裡就結束了,無論是理解,使用還是剖析原理,Condition的難度並不高,只不過大家可能平時用得比較少,因此多少有些陌生。

最後,截止到文章釋出,我應該是把開頭兩道題目的題解寫完了吧~~


好了,今天就到這裡了,Bye~~

相關文章