線程間的同步與通訊(2)——wait, notify, notifyAll

前言

上一篇文章咱們講了java的同步代碼塊, 這一篇咱們來看看同步代碼塊之間的協做與通訊.java

閱讀本篇前你須要知道什麼是同步代碼塊, 什麼是監視器鎖, 還不是很瞭解的同窗建議先去看一看上一篇文章.segmentfault

本文的源碼基於JDK1.8app

系列文章目錄less

概述

在Java中, 咱們可使用ide

  • wait()
  • wait(long timeout)
  • wait(long timeout, int nanos)
  • notify()
  • notifyAll()

這5個方法來實現同步代碼塊之間的通訊, 注意, 我說的是同步代碼塊之間的通訊, 這意味着:oop

調用該方法的當前線程必須持有對象的監視器鎖
(源碼註釋: The current thread must own this object's monitor.)

其實, 這句話換個通俗點的說法就是: 只能在同步代碼塊中使用這些方法. 源碼分析

道理很簡單, 由於只有進入了同步代塊, 才能得到監視器鎖.ui

wait方法的做用是, 阻塞當前線程(阻塞的緣由經常是一些必要的條件尚未知足), 讓出監視器鎖, 再也不參與鎖競爭, 直到其餘線程來通知(告知必要的條件已經知足了), 或者直到設定的超時等待時間到了.this

notifynotifyAll方法的做用是, 通知那些調用了wait方法的線程, 讓它們從wait處返回.線程

可見, waitnotify 方法通常是成對使用的, 我把它簡單的總結爲:

等通知

wait 是等, notify 是通知.

爲了給你們一個感性的認識, 我這裏打個比方:

假設你和舍友一塊兒租了個兩室一廳一廚一衛的房子, 天這麼熱, 固然天天都要洗澡啦, 可是衛生間只有一個, 同一時間, 只有一我的能用.

這時候, 你先下班回來了, 準備要洗澡, 剛進浴室, 忽然想起來你的專用防脫洗髮膏用完了, 查了下快遞說是1小時後才能送到, 但這時候你的舍友回來了, 他也要洗澡, 因此你總不能"站着茅坑不拉屎"吧, 因此你主動讓出了浴室(調用wait方法, 讓出監視器鎖), 讓舍友先洗, 本身快遞.

過了一個小時, 快遞送來了你的防脫洗髮膏(調用了nofity方法, 喚醒在wait中的線程), 你如今須要洗澡的資源都有了, 萬事俱備, 就差進入浴室了, 這個時候你去浴室門口一看, 嘿, 浴室空着!(當前沒有線程佔用監視器鎖) 舍友已經洗好了! 因而你高高興興的帶着你的防脫洗髮水進去洗澡了(再次得到監視器鎖).

固然, 上面還有另一種狀況, 假如你不知道快遞員何時會來, 可能在一小時後, 也多是明天, 那總不能一直乾等着不洗澡吧, 因而你決定, 我就等一個小時(調用帶超時時間的wait(long timeout)方法), 一小時後快遞還不來, 就不等了, 大不了用沐浴露湊合着洗洗頭 o(TヘTo)

上面只是拿生活中的例子打了個比方, 不知道你們理解了沒有, 下面咱們就來正經的看看代碼.

源碼分析

以上5個都方法定義在了java的Object類中, 這意味着java中全部的類都會繼承這些方法.
同時, 下面的源碼分析中咱們將看到, 這些方法都是final類型的, 也就是說全部的子類都不能改寫這些方法.

下面咱們來看源碼:

(這一段會比較長, 不想看源碼分析的能夠直接跳過這一部分看結論)

wait方法

public final void wait() throws InterruptedException {
    wait(0);
}

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException("nanosecond timeout value out of range");
    }

    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

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

wait方法共有三個, 咱們發現, 前兩個方法都是調用了最後一個方法, 而最後一個方法是一個native方法.

咱們知道, native方法是非java代碼實現的, 咱們看不到它的具體實現內容, 可是java規定了該方法要實現什麼樣的功能, 即它應該在java代碼裏"看起來是什麼樣子的".
因此native方法就像java的接口同樣, 可是具體實現由JVM直接提供,或者(更多狀況下)由外部的動態連接庫(external dynamic link library)提供,而後被JVM調用。

在Object的源碼的註釋中, 描述了該native方法"看起來應該是什麼樣子的", 咱們一段一段來看:

(這裏我把原文也貼出來了, 是怕本身翻譯的不夠精確, 英語好的能夠直接看原文)

/**
 * Causes the current thread to wait until either another thread invokes the
 * {@link java.lang.Object#notify()} method or the
 * {@link java.lang.Object#notifyAll()} method for this object, or a
 * specified amount of time has elapsed.
 * <p>
 * The current thread must own this object's monitor.
 * <p>
 ...
 */

這段是說, 該方法致使了當前線程掛起, 直到其餘線程調用了這個objectnotify或者notifyAll方法, 或者設置的超時時間到了(超時時間即timeout參數的值, 以毫秒爲單位), 另外它提到了, 當前線程必須已經拿到了監視器鎖, 這點咱們在開篇的概論中已經提到了.

/* 
 ...
 * This method causes the current thread (call it <var>T</var>) to
 * place itself in the wait set for this object and then to relinquish
 * any and all synchronization claims on this object. Thread <var>T</var>
 * becomes disabled for thread scheduling purposes and lies dormant
 * until one of four things happens:
 * <ul>
 * <li>Some other thread invokes the {@code notify} method for this
 * object and thread <var>T</var> happens to be arbitrarily chosen as
 * the thread to be awakened.
 * <li>Some other thread invokes the {@code notifyAll} method for this
 * object.
 * <li>Some other thread {@linkplain Thread#interrupt() interrupts}
 * thread <var>T</var>.
 * <li>The specified amount of real time has elapsed, more or less.  If
 * {@code timeout} is zero, however, then real time is not taken into
 * consideration and the thread simply waits until notified.
 * </ul>
 * The thread <var>T</var> is then removed from the wait set for this
 * object and re-enabled for thread scheduling. It then competes in the
 * usual manner with other threads for the right to synchronize on the
 * object; once it has gained control of the object, all its
 * synchronization claims on the object are restored to the status quo
 * ante - that is, to the situation as of the time that the {@code wait}
 * method was invoked. Thread <var>T</var> then returns from the
 * invocation of the {@code wait} method. Thus, on return from the
 * {@code wait} method, the synchronization state of the object and of
 * thread {@code T} is exactly as it was when the {@code wait} method
 * was invoked.
 ...
*/

這段話的大意是說, 該方法使得當前線程進入當前監視器鎖(this object)的等待隊列中(wait set), 而且放棄一切已經擁有的(這個監視器鎖上)的同步資源, 而後掛起當前線程, 直到如下四個條件之一發生:

  1. 其餘線程調用了this objectnotify方法, 而且當前線程剛好是被選中來喚醒的那一個(下面分析notify的時候咱們就會知道, 該方法會隨機選擇一個線程去喚醒)
  2. 其餘線程調用了this objectnotifyAll方法,
  3. 其餘線程中斷了(interrupt)了當前線程
  4. 指定的超時時間到了.(若是指定的時間是0, 則該線程會一直等待, 直到收到其餘線程的通知)

這裏插一句, 關於第四條, 解釋了無參的wait方法:

public final void wait() throws InterruptedException {
    wait(0);
}

咱們知道, 無參的wait方法的超時時間就是0, 也就是說他會無限期等待, 直到其餘線程調用了notify
或者notifyAll.

同時, 咱們再看另外一個有兩個參數的wait方法:

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException("nanosecond timeout value out of range");
    }

    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

這個方法在其源碼的註釋中號稱是實現了納秒級別的更精細的控制:

/* 
 *This method is similar to the {@code wait} method of one
 * argument, but it allows finer control over the amount of time to
 * wait for a notification before giving up. The amount of real time,
 * measured in nanoseconds, is given by:
 * <blockquote>
 * <pre>
 * 1000000*timeout+nanos</pre></blockquote>
 * <p>
 * In all other respects, this method does the same thing as the
 * method {@link #wait(long)} of one argument. In particular,
 * {@code wait(0, 0)} means the same thing as {@code wait(0)}.
 * <p>
 ...
 */

可是咱們實際看源碼可知, 當nanos的值大於0但低於999999時, 即低於1毫秒時, 就直接將timeout++了, 因此這裏哪裏來的納秒級別的控制??? 最後不仍是以毫秒爲粒度嗎? 不過是多加一毫秒而已. 這個方法真的不是在賣萌嗎?(  ̄ー ̄)

注意, 這裏一樣說明了 wait(0,0)wait(0)是等效的, 這點其實直接將值代入源碼也能得出這個結論.

好了, 吐槽完畢, 咱們接着看剩下來的註釋:

/*
 ...
 * The thread <var>T</var> is then removed from the wait set for this
 * object and re-enabled for thread scheduling. It then competes in the
 * usual manner with other threads for the right to synchronize on the
 * object; once it has gained control of the object, all its
 * synchronization claims on the object are restored to the status quo
 * ante - that is, to the situation as of the time that the {@code wait}
 * method was invoked. Thread <var>T</var> then returns from the
 * invocation of the {@code wait} method. Thus, on return from the
 * {@code wait} method, the synchronization state of the object and of
 * thread {@code T} is exactly as it was when the {@code wait} method
 * was invoked.
 ...
 */

這一段說的就是知足了上面四個條件之一以後的事情了, 此時該線程會從wait set中移除, 從新參與到線程調度中, 而且和其餘線程同樣, 競爭鎖資源, 一旦它又得到了監視器鎖, 則它在調用wait方法時的全部狀態都會被恢復, 即咱們熟知的恢復現場.

/*
 ...
 * <p>
 * A thread can also wake up without being notified, interrupted, or
 * timing out, a so-called <i>spurious wakeup</i>.  While this will rarely
 * occur in practice, applications must guard against it by testing for
 * the condition that should have caused the thread to be awakened, and
 * continuing to wait if the condition is not satisfied.  In other words,
 * waits should always occur in loops, like this one:
 * <pre>
 *     synchronized (obj) {
 *         while (&lt;condition does not hold&gt;)
 *             obj.wait(timeout);
 *         ... // Perform action appropriate to condition
 *     }
 * </pre>
 * (For more information on this topic, see Section 3.2.3 in Doug Lea's
 * "Concurrent Programming in Java (Second Edition)" (Addison-Wesley,
 * 2000), or Item 50 in Joshua Bloch's "Effective Java Programming
 * Language Guide" (Addison-Wesley, 2001).
 ...
 */

這一段是說即便沒有知足上面4個條件之一, 線程也可能被喚醒, 稱之爲假喚醒, 雖然這種狀況不多出現, 可是做者建議咱們將wait放在循環體中, 而且檢測喚醒條件是否是真的知足了, 而且還:
推薦了兩本書...
推薦了兩本書...
推薦了兩本書...
還愣着幹嗎, 趕忙去買書呀(~ ̄(OO) ̄)ブ

/*
 ...
 * <p>If the current thread is {@linkplain java.lang.Thread#interrupt()
 * interrupted} by any thread before or while it is waiting, then an
 * {@code InterruptedException} is thrown.  This exception is not
 * thrown until the lock status of this object has been restored as
 * described above.
 ...
 */

這段解釋了中斷部分, 說的是當前線程在進入wait set以前或者在wait set之中時, 若是被其餘線程中斷了, 則會拋出InterruptedException異常, 可是, 若是是在恢復現場的過程當中被中斷了, 則直到現場恢復完成後纔會拋出InterruptedException(這段不知道我理解的對不對, 由於對This exception is not thrown until the lock status of this object has been restored as described above.的翻譯不是很確信)

/*
 ...
 * <p>
 * Note that the {@code wait} method, as it places the current thread
 * into the wait set for this object, unlocks only this object; any
 * other objects on which the current thread may be synchronized remain
 * locked while the thread waits.
 * <p>
 * This method should only be called by a thread that is the owner
 * of this object's monitor. See the {@code notify} method for a
 * description of the ways in which a thread can become the owner of
 * a monitor.
 */

這段話的意思是說, 即便wait方法把當前線程放入this objectwait set裏, 也只會釋放當前監視器鎖(this object), 若是當前線程還持有了其餘同步資源, 則即便當前線程被掛起了, 也不會釋放這些資源.
同時, 這裏也提到, 該方法只能被已經持有了監視器鎖的線程所調用.

到這裏, wait方法咱們就分析完了, 雖然它是一個native方法, 源碼中並無具體實現, 可是java規定了該方法的行爲, 這些都體現了源碼的註釋中了.
同時, 咱們的分析中屢次出現了 monitor, this object, wait set等術語, 這些概念涉及到wait方法的實現細節, 咱們後面會講.

notify & notifyAll

notify和notifyAll方法都是native方法:

public final native void notify();
public final native void notifyAll();

相比於wait方法, 這兩個方法的源碼註釋要少一點, 咱們就不分段看了, 直接看所有的

notify

/**
 * Wakes up a single thread that is waiting on this object's
 * monitor. If any threads are waiting on this object, one of them
 * is chosen to be awakened. The choice is arbitrary and occurs at
 * the discretion of the implementation. A thread waits on an object's
 * monitor by calling one of the {@code wait} methods.
 * <p>
 * The awakened thread will not be able to proceed until the current
 * thread relinquishes the lock on this object. The awakened thread will
 * compete in the usual manner with any other threads that might be
 * actively competing to synchronize on this object; for example, the
 * awakened thread enjoys no reliable privilege or disadvantage in being
 * the next thread to lock this object.
 * <p>
 * This method should only be called by a thread that is the owner
 * of this object's monitor. A thread becomes the owner of the
 * object's monitor in one of three ways:
 * <ul>
 * <li>By executing a synchronized instance method of that object.
 * <li>By executing the body of a {@code synchronized} statement
 *     that synchronizes on the object.
 * <li>For objects of type {@code Class,} by executing a
 *     synchronized static method of that class.
 * </ul>
 * <p>
 * Only one thread at a time can own an object's monitor.
 *
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of this object's monitor.
 * @see        java.lang.Object#notifyAll()
 * @see        java.lang.Object#wait()
 */

上面這段是說:

  • notify方法會在全部等待監視器鎖的線程中任意選一個喚醒, 具體喚醒哪個, 交由該方法的實現者本身決定.
  • 被喚醒的線程只有等到當前持有鎖的線程徹底釋放了鎖才能繼續.(這裏解釋下, 由於調用notify方法時, 線程還在同步代碼塊裏面, 只有離開了同步代碼塊, 鎖纔會被釋放)
  • 被喚醒的線程和其餘全部競爭這個監視器鎖的線程地位是同樣的, 既不享有優先權, 也不佔劣勢.
  • 這個方法應當只被持有監視器鎖的線程調用, 一個線程能夠經過如下三種方法之一得到this object的監視器鎖:

    • 經過執行該對象的普通同步方法
    • 經過執行synchonized代碼塊, 該代碼塊以this object做爲鎖
    • 經過執行該類的靜態同步方法

咱們經過上一篇介紹synchronized同步代碼塊的文章知道, synchronized做用於類的靜態方法時, 是拿類的Class對象做爲鎖, 做用於類的普通方法或者 synchronized(this){}代碼塊時, 是拿當前類的實例對象做爲監視器鎖, 這裏的this object, 指的應該是該線程調用notify方法所持有的鎖對象.

notifyAll

/**
 * Wakes up all threads that are waiting on this object's monitor. A
 * thread waits on an object's monitor by calling one of the
 * {@code wait} methods.
 * <p>
 * The awakened threads will not be able to proceed until the current
 * thread relinquishes the lock on this object. The awakened threads
 * will compete in the usual manner with any other threads that might
 * be actively competing to synchronize on this object; for example,
 * the awakened threads enjoy no reliable privilege or disadvantage in
 * being the next thread to lock this object.
 * <p>
 * This method should only be called by a thread that is the owner
 * of this object's monitor. See the {@code notify} method for a
 * description of the ways in which a thread can become the owner of
 * a monitor.
 *
 * @throws  IllegalMonitorStateException  if the current thread is not
 *               the owner of this object's monitor.
 * @see        java.lang.Object#notify()
 * @see        java.lang.Object#wait()
 */

上面這段是說: notifyAll方法會喚醒全部等待this object監視器鎖的線程, 其餘內容和notify一致.

總結

總則: 調用這5個方法的線程必須持有監視器鎖。

  1. wait方法會使當前線程進入本身所持有的監視器鎖(this object)的等待隊列中, 而且放棄一切已經擁有的(這個監視器鎖上的)同步資源, 而後掛起當前線程, 直到如下四個條件之一發生:

    1. 其餘線程調用了this objectnotify方法, 而且當前線程剛好是被選中來喚醒的那一個
    2. 其餘線程調用了this objectnotifyAll方法,
    3. 其餘線程中斷了當前線程
    4. 指定的超時時間到了.(若是指定的超時時間是0, 則該線程會一直等待, 直到收到其餘線程的通知)
  2. 當以上4個條件之一知足後, 該線程從wait set中移除, 從新參與到線程調度中, 而且和其餘線程同樣, 競爭鎖資源, 一旦它又得到了監視器鎖, 則它在調用wait方法時的全部狀態都會被恢復, 這裏要注意「假喚醒」的問題.
  3. 當前線程在進入wait set以前或者在wait set之中時, 若是被其餘線程中斷了, 則會拋出InterruptedException異常, 可是, 若是是在恢復現場的過程當中被中斷了, 則直到現場恢復完成後纔會拋出InterruptedException
  4. 即便wait方法把當前線程放入this objectwait set裏, 也只會釋放當前監視器鎖(this object), 若是當前線程還持有了其餘同步資源, 則即便它在this object中的等待隊列中, 也不會釋放.
  5. notify方法會在全部等待監視器鎖的線程中任意選一個喚醒, 具體喚醒哪個, 交由該方法的實現者本身決定.
  6. 線程調用notify方法後不會當即釋放監視器鎖,只有退出同步代碼塊後,纔會釋放鎖(與之相對,調用wait方法會當即釋放監視器鎖)
  7. 線程被notify或notifyAll喚醒後會繼續和其餘普通線程同樣競爭鎖資源

思考題

本篇中屢次提到了monitor, this object, wait set等概念,這些都表明什麼意思?

監視器鎖究竟是怎麼獲取和釋放的?

咱們將在下一篇文章討論這個問題。

(完)

查看更多系列文章:系列文章目錄

相關文章
相關標籤/搜索