摘要:前言上一篇文章我們講了的同步代碼塊這一篇我們來看看同步代碼塊之間的協作與通信閱讀本篇前你需要知道什么是同步代碼塊什么是監視器鎖還不是很了解的同學建議先去看一看上一篇文章本文的源碼基于系列文章目錄概述在中我們可以使用這個方法來實現同步代碼塊之
前言
上一篇文章我們講了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方法的作用是, 阻塞當前線程(阻塞的原因常常是一些必要的條件還沒有滿足), 讓出監視器鎖, 不再參與鎖競爭, 直到其他線程來通知(告知必要的條件已經滿足了), 或者直到設定的超時等待時間到了.
notify和notifyAll方法的作用是, 通知那些調用了wait方法的線程, 讓它們從wait處返回.
可見, wait 和 notify 方法一般是成對使用的, 我把它簡單的總結為:
等通知
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. ** The current thread must own this object"s monitor. *
... */
這段是說, 該方法導致了當前線程掛起, 直到其他線程調用了這個object的 notify或者notifyAll方法, 或者設置的超時時間到了(超時時間即timeout參數的值, 以毫秒為單位), 另外它提到了, 當前線程必須已經拿到了監視器鎖, 這點我們在開篇的概論中已經提到了.
/* ... * This method causes the current thread (call it T) to * place itself in the wait set for this object and then to relinquish * any and all synchronization claims on this object. Thread T * becomes disabled for thread scheduling purposes and lies dormant * until one of four things happens: *
這段話的大意是說, 該方法使得當前線程進入當前監視器鎖(this object)的等待隊列中(wait set), 并且放棄一切已經擁有的(這個監視器鎖上)的同步資源, 然后掛起當前線程, 直到以下四個條件之一發生:
其他線程調用了this object的notify方法, 并且當前線程恰好是被選中來喚醒的那一個(下面分析notify的時候我們就會知道, 該方法會隨機選擇一個線程去喚醒)
其他線程調用了this object的notifyAll方法,
其他線程中斷了(interrupt)了當前線程
指定的超時時間到了.(如果指定的時間是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: **** 1000000*timeout+nanos* 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)}. *
... */
但是我們實際看源碼可知, 當nanos的值大于0但低于999999時, 即低于1毫秒時, 就直接將timeout++了, 所以這里哪里來的納秒級別的控制??? 最后不還是以毫秒為粒度嗎? 不過是多加一毫秒而已. 這個方法真的不是在賣萌嗎?(  ̄ー ̄)
注意, 這里同樣說明了 wait(0,0) 與 wait(0)是等效的, 這點其實直接將值代入源碼也能得出這個結論.
好了, 吐槽完畢, 我們接著看剩下來的注釋:
/* ... * The thread T 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 T 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方法時的所有狀態都會被恢復, 即我們熟知的恢復現場.
/* ... ** A thread can also wake up without being notified, interrupted, or * timing out, a so-called spurious wakeup. 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: *
* synchronized (obj) { * while (* (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). ... */) * obj.wait(timeout); * ... // Perform action appropriate to condition * } *
這一段是說即使沒有滿足上面4個條件之一, 線程也可能被喚醒, 稱之為假喚醒, 雖然這種情況很少出現, 但是作者建議我們將wait放在循環體中, 并且檢測喚醒條件是不是真的滿足了, 并且還:
推薦了兩本書...
推薦了兩本書...
推薦了兩本書...
還愣著干嘛, 趕緊去買書呀(~ ̄(OO) ̄)ブ
/* ... *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.的翻譯不是很確信)
/* ... ** 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. *
* 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 object的wait set里, 也只會釋放當前監視器鎖(this object), 如果當前線程還持有了其他同步資源, 則即使當前線程被掛起了, 也不會釋放這些資源.
同時, 這里也提到, 該方法只能被已經持有了監視器鎖的線程所調用.
到這里, wait方法我們就分析完了, 雖然它是一個native方法, 源碼中并沒有具體實現, 但是java規定了該方法的行為, 這些都體現了源碼的注釋中了.
同時, 我們的分析中多次出現了 monitor, this object, wait set等術語, 這些概念涉及到wait方法的實現細節, 我們后面會講.
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. ** 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. *
* 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: *
* 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. ** 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. *
* 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個方法的線程必須持有監視器鎖。
wait方法會使當前線程進入自己所持有的監視器鎖(this object)的等待隊列中, 并且放棄一切已經擁有的(這個監視器鎖上的)同步資源, 然后掛起當前線程, 直到以下四個條件之一發生:
其他線程調用了this object的notify方法, 并且當前線程恰好是被選中來喚醒的那一個
其他線程調用了this object的notifyAll方法,
其他線程中斷了當前線程
指定的超時時間到了.(如果指定的超時時間是0, 則該線程會一直等待, 直到收到其他線程的通知)
當以上4個條件之一滿足后, 該線程從wait set中移除, 重新參與到線程調度中, 并且和其他線程一樣, 競爭鎖資源, 一旦它又獲得了監視器鎖, 則它在調用wait方法時的所有狀態都會被恢復, 這里要注意“假喚醒”的問題.
當前線程在進入wait set之前或者在wait set之中時, 如果被其他線程中斷了, 則會拋出InterruptedException異常, 但是, 如果是在恢復現場的過程中被中斷了, 則直到現場恢復完成后才會拋出InterruptedException
即使wait方法把當前線程放入this object的wait set里, 也只會釋放當前監視器鎖(this object), 如果當前線程還持有了其他同步資源, 則即使它在this object中的等待隊列中, 也不會釋放.
notify方法會在所有等待監視器鎖的線程中任意選一個喚醒, 具體喚醒哪一個, 交由該方法的實現者自己決定.
線程調用notify方法后不會立即釋放監視器鎖,只有退出同步代碼塊后,才會釋放鎖(與之相對,調用wait方法會立即釋放監視器鎖)
線程被notify或notifyAll喚醒后會繼續和其他普通線程一樣競爭鎖資源
思考題本篇中多次提到了monitor, this object, wait set等概念,這些都代表什么意思?
監視器鎖到底是怎么獲取和釋放的?
我們將在下一篇文章討論這個問題。
(完)
查看更多系列文章:系列文章目錄
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76742.html
摘要:為了拓展同步代碼塊中的監視器鎖,開始,出現了接口,它實現了可定時可輪詢與可中斷的鎖獲取操作,公平隊列,以及非塊結構的鎖。 前言 系列文章目錄 前面幾篇我們學習了synchronized同步代碼塊,了解了java的內置鎖,并學習了監視器鎖的wait/notify機制。在大多數情況下,內置鎖都能很好的工作,但它在功能上存在一些局限性,例如無法實現非阻塞結構的加鎖規則等。為了拓展同步代...
摘要:在從返回前,線程與其他線程競爭重新獲得鎖。就緒隊列存儲了將要獲得鎖的線程,阻塞隊列存儲了被阻塞的線程。當線程呈狀態,調用線程對象的方法會出現異常。在執行同步代碼塊過程中,遇到異常而導致線程終止,鎖也會被釋放。 方法wait()的作用是使當前執行代碼的線程進行等待,wait()方法是Object類的方法,該方法用來將當前線程置入預執行隊列中,并且在wait()所在的代碼行處停止執行,直...
摘要:本文對多線程基礎知識進行梳理,主要包括多線程的基本使用,對象及變量的并發訪問,線程間通信,的使用,定時器,單例模式,以及線程狀態與線程組。源碼采用構建,多線程這部分源碼位于模塊中。通知可能等待該對象的對象鎖的其他線程。 本文對多線程基礎知識進行梳理,主要包括多線程的基本使用,對象及變量的并發訪問,線程間通信,lock的使用,定時器,單例模式,以及線程狀態與線程組。 寫在前面 花了一周時...
摘要:當多個線程訪問實例時,每個線程維護提供的獨立的變量副本。而則從另一個角度來解決多線程的并發訪問。在執行同步代碼塊的過程中,遇到異常而導致線程終止。在執行同步代碼塊的過程中,其他線程執行了當前對象的方法,當前線程被暫停,但不會釋放鎖。 一、Thread.start()與Thread.run()的區別通過調用Thread類的start()方法來啟動一個線程,這時此線程是處于就緒狀態,并沒有...
摘要:線程通信的目標是使線程間能夠互相發送信號。但是,這個標志已經被第一個喚醒的線程清除了,所以其余醒來的線程將回到等待狀態,直到下次信號到來。如果方法調用,而非,所有等待線程都會被喚醒并依次檢查信號值。 線程通信的目標是使線程間能夠互相發送信號。另一方面,線程通信使線程能夠等待其他線程的信號。 showImg(http://segmentfault.com/img/bVbPLD); 例...
閱讀 3715·2021-10-14 09:43
閱讀 3311·2021-08-25 09:38
閱讀 609·2019-08-30 15:55
閱讀 1343·2019-08-30 13:05
閱讀 2238·2019-08-29 16:05
閱讀 501·2019-08-29 12:58
閱讀 2791·2019-08-29 12:34
閱讀 3241·2019-08-26 12:15