Java 多線程(五)- 理解 Condition 和 條件變量

在關(guān)于 ReentrantLock 的文章中,提到 Lock 接口作為內(nèi)置 Monitor 鎖的補充,提供了更靈活的接口,其中 lock / unlock 對于內(nèi)置鎖的 synchronized,那么內(nèi)置鎖的 監(jiān)控條件 對應(yīng) Lock 的什么呢?就是 newCondition 返回的 Condition。Condition 和 內(nèi)置鎖的監(jiān)控條件都被叫做 條件變量

條件變量

作用

條件變量最主要的作用是用來管理線程執(zhí)行對某些狀態(tài)的依賴性。想象一下:一個線程是某個隊列的消費者,它必須要等到隊列中有數(shù)據(jù)時才能執(zhí)行,如果隊列為空,則會一直等待掛起,直到另外一個線程在隊列中存入數(shù)據(jù),并通知先前掛起的線程,該線程才會喚醒重新開始執(zhí)行。

這個例子中,隊列是否空/滿 是線程執(zhí)行所依賴的狀態(tài),而這個狀態(tài)是多個線程需要訪問的,所以需要加鎖互斥訪問,這種加鎖模式與其他同步加鎖略有不同,鎖在操作的執(zhí)行過程中需要被釋放與重新獲取的。管理依賴共享變量的線程執(zhí)行通常用如下的編程模式:

獲取鎖;
while (條件狀態(tài)不滿足) {
    釋放鎖;
    線程掛起等待,直到條件滿足通知;
    重新獲取鎖;
}

臨界區(qū)操作;
釋放鎖;

條件變量為了管理這種依賴性,需要做兩件事情:

  1. 提供 await / wait 接口,掛起當前線程,并將線程放入條件隊列 管理,同時釋放鎖;
  2. 提供 signal / notify 接口,喚醒等待的線程,重新?lián)屾i運行;

在編程模式里為什么要使用 while 而不是 if,已經(jīng)在之前的 Monitor 內(nèi)置鎖中有所闡述。

條件隊列

條件隊列來源于:它使得一組線程(等待線程集合)能夠通過某種方式來等待特條件變成真。傳統(tǒng)隊列的元素是一個個數(shù)據(jù),而與之不同的是,條件隊列中的元素是一個個正在等待相關(guān)條件的線程。

內(nèi)置鎖中的條件隊列

前面的文章說過,每個 Java 對象都是一個 Monitor Object 模式的對象,可以當作一個 Monitor 鎖。每個對象同樣可以作為一個條件隊列,提供了 wait / notify / notifyAll 方法構(gòu)成內(nèi)部條件隊列的 API。

Object.wait 會自動釋放內(nèi)置鎖,并請求操作系統(tǒng)掛起當前線程,從而使其他線程能夠獲得內(nèi)置鎖,修改依賴的對象狀態(tài)。當掛起的線程醒來時,它將在返回之前重新獲取鎖。

使用 wait / notify 組合接口管理狀態(tài)依賴性比“輪詢和休眠”更加簡單和高效。

輪詢是指在 while 循環(huán)里不斷檢查條件狀態(tài),如果條件狀態(tài)滿足,則進行以下處理,這會浪費很多 CPU 時鐘進行判斷。

休眠是指在 while 循環(huán)里檢查條件狀態(tài),如果狀態(tài)不滿足,則 sleep 一段時間,線程醒來后則再次判斷。它比輪詢節(jié)約 CPU 時間片,但比條件變量低效,而且 sleep 的時間間隔難以把握,會依賴狀態(tài)改變后也不會立即醒來,響應(yīng)性也比條件隊列差。

但是在功能實現(xiàn)上,這幾種方式并沒有差別,也就是說:

如果某個功能無法通過“輪詢和休眠”來實現(xiàn),那么條件隊列也無法實現(xiàn)

條件謂詞

要想正確使用條件隊列,關(guān)鍵是找出對象在哪個條件謂詞上等待。條件謂詞并不依賴于條件變量的接口,它是使某個操作稱為狀態(tài)依賴操作的前提條件。如下圖的代碼塊:

圖1 使用內(nèi)置鎖條件變量

其中2處的 isFull 函數(shù)就是一個條件謂詞,表示“隊列已滿”時,需要等待。

三元關(guān)系

在條件等待中存在一種重要的三元關(guān)系,包括加鎖,wait 方法和一個條件謂詞。

在條件謂詞中包含一個或多個線程共享的狀態(tài)變量,需要一個鎖來保護。因此在測試條件謂詞之前必須要先持有鎖。鎖對象與條件隊列對象必須是同一個對象,他們之間的三元關(guān)系如下:

每一次 wait 調(diào)用都會隱式地與特定的條件謂詞關(guān)聯(lián)起來;

當調(diào)用某個特定條件謂詞的 wait 時,調(diào)用著必須已經(jīng)持有與條件隊列相關(guān)的鎖;

并且這個鎖必須保護著構(gòu)成條件謂詞的狀態(tài)變量。

內(nèi)置 Monitor 條件變量缺陷
過早喚醒

雖然鎖 / 條件謂詞 / 條件隊列之間的三元關(guān)系不是很復(fù)雜,但 wait 方法的返回并不一定意味著線程正在等待的條件謂詞已經(jīng)成真??紤]圖 2 的阻塞隊列代碼段:

圖2 某阻塞隊列代碼片段

假設(shè)有 A,B 兩條線程阻塞在 put 函數(shù),C 線程調(diào)用 take,獲取并推出隊列中一個數(shù)據(jù),同時調(diào)用 notifyAll 喚醒 A,B 線程;若 A 線程獲取內(nèi)置鎖,B 阻塞在鎖獲取中,A 又向隊列壓入一個數(shù)據(jù),此時隊列又滿了;A 釋放鎖后,B 獲取鎖,但是隊列已滿,條件謂詞判斷失敗,再次 wait 阻塞。

信號丟失

這里的信號丟失是指:線程正在等待一個已經(jīng)(或者本應(yīng)該)發(fā)生過的喚醒信號。錯誤的編程模式通常會造成信號丟失。考慮圖 3 的阻塞隊列代碼段:

圖2 某阻塞隊列代碼片段

假設(shè)有 A 線程阻塞在 put 函數(shù),B 線程阻塞在 take 函數(shù),C 線程調(diào)用 take,然后使用 notify 接口喚醒其中一個線程;不巧的是 B 線程被喚醒,B 檢查隊列仍然為空,繼續(xù)等待阻塞,此時應(yīng)該被喚醒的 A 只能等待下一個喚醒。

Condition

Condition VS Monitor 條件變量

分析內(nèi)置 Monitor 條件變量的過早喚醒和信號丟失,它們其實有一個共同的原因:多個線程在同一個條件隊列上等待不同的條件謂詞。如果想編寫一個帶有多個條件謂詞的并發(fā)對象,或者想除了條件隊列可見性意外的更多控制權(quán),就可以使用顯示的 Lock 和 Condition 而不是內(nèi)置鎖和條件隊列。

與內(nèi)置條件隊列不同,對于每一個 Lock,可以有任意數(shù)量的 Condition 對象,因此對于不同的條件謂詞,對于同一個鎖,可以用不同的 Condition 對象來控制。

同時類似于 Lock 和內(nèi)置鎖的差異,Condition 也提供了豐富的接口等待掛起(可輪詢,可中斷,可超時等)。接口如下所示:

// wait 接口
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;

// notify
void signal();
void signalAll();

Condition 對象會繼承相關(guān)的 Lock 公平性,對于公平的鎖,線程會依照 FIFO 順序從 await 中釋放。

特別注意:在 Condition 對象中,與 wait,notify, notifyAll 方法對應(yīng)的分別是 await,signal 和 signalAll。但是實現(xiàn) Condition 的類必然繼承自 Object,因為它也包含了 wait 和 notify 方法。所以使用時一定要確保正確的版本。

分析代碼

深究下,Condition 是如何管理隊列的,它為什么會繼承 Lock 的公平性,Condition 是如何阻塞擁有鎖的線程。介紹完 Condition 后,可能會冒出更多的問題,為了學(xué)習 Condition,不妨以 AQS 的 ConditionObject 作為代碼分析對象理解理解。

Node 隊列節(jié)點

ConditionObject 復(fù)用了和 AQS 的隊列節(jié)點 Node(具體可查看上篇文章),不同的:

  1. waitStatus 值為 CONDITION(-2), 表示該節(jié)點在條件隊列上。
  2. nextWaiter 指向條件隊列的下一個節(jié)點。

同時在 ConditionObject 內(nèi)保存有隊列的首尾指針:

  1. firstWaiter,指向隊列的第一個Node
  2. lastWaiter,指向隊列最后一個Node

下文為了區(qū)分兩個不同的隊列,使用以下名詞:

  1. 同步隊列:AQS 中的鎖等待隊列
  2. 條件隊列:ConditionObject 中的條件隊列
await

簡單起見,我們分析方法 awaitUninterruptibly,代碼片段如下圖所示

圖4 awaitUninterruptibly
  • 1972行,addConditionWaiter 方法會在 ConditionObject 隊列尾部插入一個代表當前線程的 Node,狀態(tài)為 CONDITION;
  • 1973行,因為要調(diào)用 await 接口之前一定已經(jīng)獲得鎖,所以當前線程在同步隊列中一定是首節(jié)點,AQS.fullyRelease 釋放當前鎖,恢復(fù)同步隊列后續(xù)節(jié)點執(zhí)行,返回當前的許可狀態(tài)用于重新申請鎖;
  • 1975行,AQS.OnSyncQueue 用來判斷當前線程節(jié)點是否在同步隊列中。為了防止被誤喚醒,此處采用 while 進行輪詢判斷;
  • 1976行,使用 LockSupport.park 掛起當前線程;
  • 1980行,被其他線程喚醒后,調(diào)用 AQS.acquireQueued 重新嘗試獲取鎖,如果獲取失敗則被加入同步隊列,AQS.acquireQueued 會調(diào)用 AQS.tryAcquire 獲取準入許可,所以 ConditonObject 繼承了 AQS 的公平性。
signal

ConditionObject 的 signal 方法比較簡單,主要代碼被封裝在 doSignal,該方法如下圖所示:

圖5 doSignal
  • 1874行,為轉(zhuǎn)移線程節(jié)點做準備,將 nextWaiter 設(shè)置為 null,在同步隊列,該字段無用,設(shè)置為 null 后,利于以后垃圾回收;
  • 1875行,關(guān)鍵是 transferForSignal,它主要干以下這些事:
    1. 使用 CAS 設(shè)置節(jié)點狀態(tài)為 0;
    2. 調(diào)用 AQS.enq 將 node 重新壓入同步隊列;
    3. 修改 node 的前繼節(jié)點狀態(tài)為 SIGNAL;
    4. 如果前繼節(jié)點已經(jīng)取消等待,恢復(fù)該 node 代表的線程

編程實踐

以下代碼是結(jié)合 Lock 和 Condition 實現(xiàn)容量為100的阻塞線程:

class BoundedBuffer<V> {
        final Lock lock = new ReentrantLock();//鎖對象
        final Condition notFull  = lock.newCondition();//寫線程條件變量
        final Condition notEmpty = lock.newCondition();//讀線程條件變量
        
        final LinkedList<V> items = new LinkedList<V>();
        final int totalCount = 100;

        public void put(V x) throws InterruptedException {
                lock.lock();
                try {
                    while (totalCount >= items.size())//如果隊列滿了
                        notFull.await();//阻塞寫線程

                    items.addLast(x);

                    notEmpty.signal();//喚醒讀線程
                } finally {
                    lock.unlock();
                }
        }

        public V take() throws InterruptedException {
                lock.lock();
                try {
                    while (items.size() == 0)//如果隊列為空
                        notEmpty.await();//阻塞讀線程

                    V x = items.removeFirst();
                    notFull.signal();//喚醒寫線程
                    return x;
            } finally {
                    lock.unlock();
                }
        }
}

代碼中 notFull 代表了寫線程條件變量,notEmpty 代表了讀線程條件變量,在 put 的時候?qū)懭霐?shù)據(jù),signal 只會喚醒等待在 notEmpty 的線程;對應(yīng)的 take 取出數(shù)據(jù)后,喚醒的也只會是等待在 notFull 的線程。

Condition 比內(nèi)置鎖的條件隊列做的更加細致,能夠很好的解決過早喚醒和信號丟失的問題。

內(nèi)容來源

Java 并發(fā)編程實戰(zhàn)

http://blog.csdn.net/ghsau/article/details/7481142

http://blog.csdn.net/vernonzheng/article/details/8288251

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容