併發王者課-鉑金6:青出於藍-Condition如何把等待與通知玩出新花樣

秦二爺發表於2021-07-01

歡迎來到《併發王者課》,本文是該系列文章中的第19篇

在上一篇文章中,我們介紹了阻塞佇列。如果你閱讀過它的原始碼,那麼你一定會注意到原始碼有兩個Condition型別的變數:notEmptynotFull,在讀寫佇列時你也會注意到它們是如何被使用的。事實上,在使用JUC中的各種鎖時,Condition都很有用場,你很有必要了解它。所以,本文就為你介紹它的來龍去脈和用法。

在前面的系列文章中,我們多次提到過synchronized關鍵字,相信你已經對它的用法瞭如於心。在多執行緒協作時,有兩個另外的關鍵字經常和synchronized一同出現,它們相互配合,就是waitnotify,比如下面的這段程式碼:

public class CountingSemaphore {
private int signals = 0;
public synchronized void take() {
this.signals++;
this.notify(); // 傳送通知
}
public synchronized void release() throws InterruptedException {
while (this.signals == 0)
this.wait(); // 釋放鎖,進入等待
This.signals--;
}
}

synchronized是Java的原生同步工具,waitnotify是它的原生搭檔。然而,在鉑金系列中,我們已經開始了Lock介面和它的一些實現,比如可重入鎖ReentrantLock等。相比於synchronized,JUC所封裝的這些鎖工具在功能上要豐富得多,也更加容易使用。所以,相應地配套自然也要跟上,於是Condition就應運而生。

比如在上文的阻塞佇列中,Condition就已經閃亮登場:

public class LinkedBlockingQueue < E > extends AbstractQueue < E >
implements BlockingQueue < E > , java.io.Serializable {

...省略原始碼若干

// 定義Condition
// 注意,這裡定義兩個Condition物件,用於喚醒不同的執行緒
private final Condition notEmpty = takeLock.newCondition();
private final Condition notFull = putLock.newCondition();

public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
// 進入等待
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}

private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
// 傳送喚醒訊號
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
...省略原始碼若干

}

從功能定位上說,作為Lock的配套工具,Condition是waitnotifynotifyAll增強版本,waitnotify有的能力它都有,waitnotify沒有的能力它也有

JUC中的Condition是以介面的形式出現,並定義了一些核心方法:

  • await():讓當前執行緒進入等待,直到收到訊號或者被中斷;

  • await(long time, TimeUnit unit):讓當前執行緒進入等待,直到收到訊號或者被中斷,或者到達指定的等待超時時間;

  • awaitNanos(long nanosTimeout):讓當前執行緒進入等待,直到收到訊號或者被中斷,或者到達指定的等待超時時間,只是在時間單位上和上一個方法有所區別;

  • awaitUninterruptibly()讓當前執行緒進入等待,直到收到訊號。注意,這個方法對中斷是不敏感的

  • awaitUntil(Date deadline)讓當前執行緒進入等待,直到收到訊號或者被中斷,或者到達截止時間

  • signal():隨機喚醒一個執行緒;

  • signalAll():喚醒所有等待的執行緒。

從Condition的核心方法中可以看到,相較於原生的通知與等待,它的能力明顯增強了很多,比如awaitUninterruptibly()awaitUntil()。另外,Condition竟然是可以喚醒指定執行緒的,這就很有意思

作為介面,我們並不需要手動實現Condition,JUC已經提供了相關的實現,你可以在ReentrantLock中直接使用它。相關的類、介面之間的關係如下所示:

小結

以上就是關於Condition的全部內容。Condition並不複雜,它是JUC中Lock的配套,在理解時要結合原生的waitnotify去理解。關於Condition與它們之間的詳細區別,已經都在下面的表格裡:

對比項 Object's Monitor methods Condition
前置條件 獲取物件的鎖 呼叫Lock獲取鎖,呼叫lock.newCondition()獲取Condition物件
呼叫方式 直接呼叫,如object.wait() 直接呼叫,如condition.await()
等待佇列個數 一個 多個
當前執行緒釋放鎖並進入等待狀態 ✔︎ ✔︎
當前執行緒釋放鎖並進入等待狀態,在等待時不響應中斷 ✔︎
當前執行緒釋放鎖並進入超時等待 ✔︎ ✔︎
當前執行緒釋放鎖並進入等待到未來某個時刻 ✔︎
喚醒等待佇列中的某一個執行緒 ✔︎ ✔︎
喚醒等待佇列中的全部執行緒 ✔︎ ✔︎

理解表格中的各項差異,不要死記硬背,而是要基於Condition介面中定義的方法,從關鍵處理解它們的不同。

正文到此結束,恭喜你又上了一顆星✨

夫子的試煉

  • 編寫程式碼使用Condition喚醒指定執行緒。

延伸閱讀與參考資料

最新修訂及更好閱讀體驗

關於作者

關注公眾號【技術八點半】,及時獲取文章更新。傳遞有品質的技術文章,記錄平凡人的成長故事,偶爾也聊聊生活和理想。早晨8:30推送作者品質原創,晚上20:30推送行業深度好文。

如果本文對你有幫助,歡迎點贊關注監督,我們一起從青銅到王者

相關文章