執行緒間的同步與通訊(2)——wait, notify, notifyAll

ChiuCheng發表於2019-01-19

前言

上一篇文章我們講了java的同步程式碼塊, 這一篇我們來看看同步程式碼塊之間的協作與通訊.

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

本文的原始碼基於JDK1.8

系列文章目錄

概述

在Java中, 我們可以使用

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

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

呼叫該方法的當前執行緒必須持有物件的監視器鎖
(原始碼註釋: The current thread must own this object`s monitor.)

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

道理很簡單, 因為只有進入了同步代塊, 才能獲得監視器鎖.

wait方法的作用是, 阻塞當前執行緒(阻塞的原因常常是一些必要的條件還沒有滿足), 讓出監視器鎖, 不再參與鎖競爭, 直到其他執行緒來通知(告知必要的條件已經滿足了), 或者直到設定的超時等待時間到了.

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等概念,這些都代表什麼意思?

監視器鎖到底是怎麼獲取和釋放的?

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

(完)

檢視更多系列文章:系列文章目錄

相關文章