引入
上一篇關于Condition,我們對Condition有了進一步了解,在之前生產/消費者模式一文,我們講過如何用 Condition 和 wait/notify 來實現生產者/消費者模式,其中的精髓就在于用Condition 和 wait/notify 來實現簡易版阻塞隊列,我們先來分別回顧一下這兩段代碼。
用 Condition 實現簡易版阻塞隊列
代碼如下所示:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/*** 一個使用 Condition 實現的阻塞隊列類。* 該類提供了一個線程安全的隊列,支持在隊列滿時阻塞插入操作,* 在隊列空時阻塞移除操作。*/
public class MyBlockingQueueForCondition {// 存儲元素的隊列private Queue queue;// 隊列的最大容量private int max = 16;// 用于線程同步的可重入鎖private ReentrantLock lock = new ReentrantLock();// 當隊列不為空時的條件private Condition notEmpty = lock.newCondition();// 當隊列不為滿時的條件private Condition notFull = lock.newCondition();/*** 構造函數,初始化隊列的最大容量。* * @param size 隊列的最大容量*/public MyBlockingQueueForCondition(int size){this.max = size;queue = new LinkedList();}/*** 向隊列中插入一個元素。* 如果隊列已滿,線程將被阻塞,直到隊列有空間。* * @param o 要插入的元素* @throws InterruptedException 如果線程在等待時被中斷*/public void put(Object o) throws InterruptedException {// 獲取鎖lock.lock();try {// 當隊列已滿時,線程等待while (queue.size() == max) {notFull.await();}// 向隊列中添加元素queue.add(o);// 通知所有等待隊列不為空的線程notEmpty.signalAll();} finally {// 釋放鎖lock.unlock();}}/*** 從隊列中移除并返回一個元素。* 如果隊列為空,線程將被阻塞,直到隊列中有元素。* * @return 隊列中的第一個元素* @throws InterruptedException 如果線程在等待時被中斷*/public Object take() throws InterruptedException {// 獲取鎖lock.lock();try {// 當隊列為空時,線程等待while (queue.size() == 0) {notEmpty.await();}// 從隊列中移除并獲取元素Object item = queue.remove();// 通知所有等待隊列不為滿的線程notFull.signalAll();return item;} finally {// 釋放鎖lock.unlock();}}
}
在上面的代碼中,首先定義了一個隊列變量 queue,其最大容量是 16;然后定義了一個ReentrantLock 類型的 Lock 鎖,并在 Lock 鎖的基礎上創建了兩個 Condition,一個是 notEmpty,另一個是 notFull,分別代表隊列沒有空和沒有滿的條件;最后,聲明了 put 和 take 這兩個核心方法。
用 wait/notify 實現簡易版阻塞隊列
我們再來看看如何使用 wait/notify 來實現簡易版阻塞隊列,代碼如下:
import java.util.LinkedList;
/*** 自定義阻塞隊列類,使用 wait() 和 notifyAll() 方法實現線程同步。*/
public class MyBlockingQueueForWaitNotify {/*** 隊列的最大容量*/private int maxSize;/*** 存儲隊列元素的鏈表*/private LinkedList<Object> storage;/*** 構造函數,初始化隊列的最大容量和存儲鏈表。** @param size 隊列的最大容量*/public MyBlockingQueueForWaitNotify (int size) {// 將傳入的最大容量賦值給類的成員變量this.maxSize = size;// 初始化存儲鏈表storage = new LinkedList<>();}/*** 向隊列中添加一個元素。如果隊列已滿,則線程進入等待狀態。** @throws InterruptedException 如果線程在等待過程中被中斷*/public synchronized void put() throws InterruptedException {// 當隊列已滿時,當前線程進入等待狀態while (storage.size() == maxSize) {this.wait();}// 向隊列中添加一個新元素storage.add(new Object());// 喚醒所有等待的線程this.notifyAll();}/*** 從隊列中取出一個元素。如果隊列為空,則線程進入等待狀態。** @throws InterruptedException 如果線程在等待過程中被中斷*/public synchronized void take() throws InterruptedException {// 當隊列為空時,當前線程進入等待狀態while (storage.size() == 0) {this.wait();}// 從隊列中移除并打印第一個元素System.out.println(storage.remove());// 喚醒所有等待的線程this.notifyAll();}
}
如代碼所示,最主要的部分仍是 put 與 take 方法。我們先來看 put 方法,該方法被 synchronized 保護,while 檢查 List 是否已滿,如果不滿就往里面放入數據,并通過 notifyAll() 喚醒其他線程。同樣,take 方法也被 synchronized 修飾,while 檢查 List 是否為空,如果不為空則獲取數據并喚醒其他線程。
在生產/消費者模式中,有對這兩段代碼的詳細講解,遺忘的小伙伴可以到前面復習一下。
Condition 和 wait/notify的關系
對比上面兩種實現方式的 put 方法,會發現非常類似,此時讓我們把這兩段代碼同時列在屏幕中,然后進行對比:
public void put(Object o) throws InterruptedException {lock.lock();try {while (queue.size() == max) {notFull.await();}queue.add(o);notEmpty.signalAll();} finally {lock.unlock();} } | public synchronized void put() throws InterruptedException {while (storage.size() == maxSize) {this.wait();}storage.add(new Object()); this.notifyAll(); } |
可以看出,左側是 Condition 的實現,右側是 wait/notify 的實現:
- lock.lock() 對應進入 synchronized 方法
- condition.await() 對應 object.wait()
- condition.signalAll() 對應 object.notifyAll()
- lock.unlock() 對應退出 synchronized 方法
實際上,如果說 Lock 是用來代替 synchronized 的,那么 Condition 就是用來代替相對應的 Object 的wait/notify/notifyAll,所以在用法和性質上幾乎都一樣。
Condition 把 Object 的 wait/notify/notifyAll 轉化為了一種相應的對象,其實現的效果基本一樣,但是把更復雜的用法,變成了更直觀可控的對象方法,是一種升級。
await 方法會自動釋放持有的 Lock 鎖,和 Object 的 wait 一樣,不需要自己手動釋放鎖。
另外,調用 await 的時候必須持有鎖,否則會拋出異常,這一點和 Object 的 wait 一樣。
總結
Condition 是對 wait/notify 的改進和擴展,提供了更高的靈活性和可讀性。如果需要更復雜的線程通信機制,建議使用 Condition;如果場景簡單,可以繼續使用 wait/notify。
下面我們梳理總結一下,核心異同,以及各自適用場景:
相似點
目的相同:兩者都是用于實現線程間的通信和同步。它們允許一個線程等待某個條件滿足,然后由另一個線程通知它條件已經滿足,從而繼續執行。
等待和通知機制:都涉及線程進入等待狀態,然后被其他線程通知喚醒。在等待期間,線程會釋放鎖,以便其他線程可以進入同步塊修改共享狀態。
線程通信:兩者都用于線程間的通信,允許線程等待或喚醒其他線程。
需要鎖:兩者都需要與鎖配合使用,wait/notify 依賴 synchronized,而 Condition 依賴 Lock。
不同點
特性 | wait/notify | Condition |
---|---|---|
鎖的管理 | 隱式鎖(通過 synchronized) | 顯式鎖(通過 Lock) |
靈活性 | 較低,只能有一個等待隊列 | 較高,可以有多個條件變量(多個等待隊列) |
可讀性 | 較低,代碼容易變得復雜 | 較高,代碼更清晰 |
中斷處理 | 不支持中斷 | 支持中斷(awaitUninterruptibly() 等) |
等待條件 | 無法指定多個條件 | 可以指定多個條件(newCondition()) |
適用場景
wait/notify:適用于簡單的線程通信場景。
在 Java 中,wait、notify和notifyAll是Object類的方法。當一個線程調用一個對象的wait方法時,它會進入等待狀態,直到另一個線程調用同一個對象的notify或notifyAll方法。通常在使用synchronized關鍵字實現同步的時候使用。例如,一個線程在同步塊中調用wait方法等待某個條件,另一個線程在同步塊中改變了這個條件后調用notify或notifyAll方法通知等待的線程。
Condition:適用于復雜的線程通信場景,尤其是需要多個條件變量的場景。
在 Java 中,Condition是在java.util.concurrent.locks包下的一個接口,它是對傳統的對象監視器方法(如wait、notify和notifyAll)的一種替代,用于更靈活地實現線程間的通信和等待。通常在使用ReentrantLock實現同步的時候,配合Condition來實現線程間的等待和通知。比如,當一個線程需要等待某個條件滿足時,它可以調用Condition的await方法進入等待狀態,直到另一個線程調用signal或signalAll方法來通知它條件已經滿足。