天天看點

如何在 Java 中正确使用 wait, notify 和 notifyAll – 以生産者消費者模型為例

wait, notify 和 notifyAll,這些在多線程中被經常用到的保留關鍵字,在實際開發的時候很多時候卻并沒有被大家重視。本文對這些關鍵字的使用進行了描述。

在 Java 中可以用 wait、notify 和 notifyAll 來實作線程間的通信。。舉個例子,如果你的Java程式中有兩個線程——即生産者和消費者,那麼生産者可以通知消費者,讓消費者開始消耗資料,因為隊列緩沖區中有内容待消費(不為空)。相應的,消費者可以通知生産者可以開始生成更多的資料,因為當它消耗掉某些資料後緩沖區不再為滿。

我們可以利用wait()來讓一個線程在某些條件下暫停運作。例如,在生産者消費者模型中,生産者線程在緩沖區為滿的時候,消費者在緩沖區為空的時候,都應該暫停運作。如果某些線程在等待某些條件觸發,那當那些條件為真時,你可以用 notify 和 notifyAll 來通知那些等待中的線程重新開始運作。不同之處在于,notify 僅僅通知一個線程,并且我們不知道哪個線程會收到通知,然而 notifyAll 會通知所有等待中的線程。換言之,如果隻有一個線程在等待一個信号燈,notify和notifyAll都會通知到這個線程。但如果多個線程在等待這個信号燈,那麼notify隻會通知到其中一個,而其它線程并不會收到任何通知,而notifyAll會喚醒所有等待中的線程。

盡管關于wait和notify的概念很基礎,它們也都是Object類的函數,但用它們來寫代碼卻并不簡單。如果你在面試中讓應聘者來手寫代碼,用wait和notify解決生産者消費者問題,我幾乎可以肯定他們中的大多數都會無所适從或者犯下一些錯誤,例如在錯誤的地方使用 synchronized 關鍵詞,沒有對正确的對象使用wait,或者沒有遵循規範的代碼方法。說實話,這個問題對于不常使用它們的程式員來說确實令人感覺比較頭疼。

第一個問題就是,我們怎麼在代碼裡使用wait()呢?因為wait()并不是Thread類下的函數,我們并不能使用Thread.call()。事實上很多Java程式員都喜歡這麼寫,因為它們習慣了使用Thread.sleep(),是以他們會試圖使用wait() 來達成相同的目的,但很快他們就會發現這并不能順利解決問題。正确的方法是對在多線程間共享的那個Object來使用wait。在生産者消費者問題中,這個共享的Object就是那個緩沖區隊列。

第二個問題是,既然我們應該在synchronized的函數或是對象裡調用wait,那哪個對象應該被synchronized呢?答案是,那個你希望上鎖的對象就應該被synchronized,即那個在多個線程間被共享的對象。在生産者消費者問題中,應該被synchronized的就是那個緩沖區隊列。(我覺得這裡是英文原文有問題……本來那個句末就不應該是問号不然不太通……)

<a target="_blank" href="http://jbcdn2.b0.upaiyun.com/2015/07/5fda7f863416140cb97a8c8977dfd5db.png"></a>

基于以上認知,下面這個是使用wait和notify函數的規範代碼模闆:

就像我之前說的一樣,在while循環裡使用wait的目的,是線上程被喚醒的前後都持續檢查條件是否被滿足。如果條件并未改變,wait被調用之前notify的喚醒通知就來了,那麼這個線程并不能保證被喚醒,有可能會導緻死鎖問題。

下面我們提供一個使用wait和notify的範例程式。在這個程式裡,我們使用了上文所述的一些代碼規範。我們有兩個線程,分别名為PRODUCER(生産者)和CONSUMER(消費者),他們分别繼承了了Producer和Consumer類,而Producer和Consumer都繼承了Thread類。Producer和Consumer想要實作的代碼邏輯都在run()函數内。Main線程開始了生産者和消費者線程,并聲明了一個LinkedList作為緩沖區隊列(在Java中,LinkedList實作了隊列的接口)。生産者在無限循環中持續往LinkedList裡插入随機整數直到LinkedList滿。我們在while(queue.size

== maxSize)循環語句中檢查這個條件。請注意到我們在做這個檢查條件之前已經在隊列對象上使用了synchronized關鍵詞,因而其它線程不能在我們檢查條件時改變這個隊列。如果隊列滿了,那麼PRODUCER線程會在CONSUMER線程消耗掉隊列裡的任意一個整數,并用notify來通知PRODUCER線程之前持續等待。在我們的例子中,wait和notify都是使用在同一個共享對象上的。

<a target="_blank" href="http://jbcdn2.b0.upaiyun.com/2015/07/19b8448d72b097bda0616b703b0d7db3.png"></a>

into和Step over按鈕來更好地了解多線程間發生的事情。

1. 你可以使用wait和notify函數來實作線程間通信。你可以用它們來實作多線程(&gt;3)之間的通信。

2. 永遠在synchronized的函數或對象裡使用wait、notify和notifyAll,不然Java虛拟機會生成 IllegalMonitorStateException。

3. 永遠在while循環裡而不是if語句下使用wait。這樣,循環會線上程睡眠前後都檢查wait的條件,并在條件實際上并未改變的情況下處理喚醒通知。

4. 永遠在多線程間共享的對象(在生産者消費者模型裡即緩沖區隊列)上使用wait。

5. 基于前文提及的理由,更傾向用 notifyAll(),而不是 notify()。

<a target="_blank" href="http://jbcdn2.b0.upaiyun.com/2015/07/fb4daecd5650cb2c82b9eb7cd9d54e3f.png"></a>

繼續閱讀