大佬問我: notify()會立刻釋放鎖麼?
我的内心戲: 肯定會啊! 這麼簡單的問題?
image
聰明如我, 決定裝小白, 回答: 不會?
大佬: 很好, 小夥子基礎不錯!
我:
image
大佬: 說說為什麼
我: ………………
image
于是, 有了這篇文章!
問題的根本原來在于 “立刻”這個描述詞!
如果你和鹹魚君一樣懵逼, 不妨往下看!
技術大佬可以告辭了!!
接下來, 我們深入的分析分析wait和notify
前言
前面介紹了Synchronized關鍵詞的原理與優化分析,Synchronized的重要不言而喻, 而作為配合Synchronized使用的另外兩個關鍵字也顯得格外重要.
今天, 來聊聊配合Object基類的
- wait()
- notify()
這兩個方法的實作,為多線程協作提供了保證。
wait() & notify()
Object 類中的 wait¬ify 這兩個方法,其實包括他們的重載方法一共有 5 個,而 Object 類中一共才 12 個方法,可見這 2 個方法的重要性。
我們先看看 JDK 中的定義:
public final native void notify();
其中有 3 個方法是 native 的,也就是由虛拟機本地的 c 代碼執行的。
ps: native 即 JNI,Java Native Interface,
Java平台提供的使用者和本地C代碼進行互操作的API
有 2 個 wait 重載方法最終還是調用了 wait(long)方法。
wait方法
wait是要釋放對象鎖,進入等待池。
既然是釋放對象鎖,那麼肯定是先要獲得鎖。
是以wait必須要寫在synchronized代碼塊中,否則會報異常。
notify方法
也需要寫在synchronized代碼塊中,
調用對象的這兩個方法也需要先獲得該對象的鎖.
notify,notifyAll, 喚醒等待該對象同步鎖的線程,并放入該對象的鎖池中.
對象的鎖池中線程可以去競争得到對象鎖,然後開始執行.
如果是通過notify來喚起的線程,
那進入wait的線程會被随機喚醒;
(注意: 實際上, hotspot是順序喚醒的!! 這是個重點! 有疑惑的點選傳送大佬問我: notify()是随機喚醒線程麼?
)
如果是通過notifyAll喚起的線程,
預設情況是最後進入的會先被喚起來,即LIFO的政策;
比較重要的是:
notify()或者notifyAll()調用時并不會真正釋放對象鎖, 必須等到synchronized方法或者文法塊執行完才真正釋放鎖.
舉個例子:
public void test()
{
Object object = new Object();
synchronized (object){
object.notifyAll();
while (true){
}
}
}
如上, 雖然調用了notifyAll, 但是緊接着進入了一個死循環。
這會導緻一直不能出臨界區, 一直不能釋放對象鎖。
是以,即使它把所有在等待池中的線程都喚醒放到了對象的鎖池中,
但是鎖池中的所有線程都不會運作,因為他們始終拿不到鎖。
案例分析
為了說明wait() 和notify()方法的功能,
我們舉個例子
public class WaitNotifyCase {
public static void main(String[] args) {
final Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("線程 A 等待 獲得 鎖");
synchronized (lock) {
try {
System.out.println("線程 A 獲得 鎖");
TimeUnit.SECONDS.sleep(1);
System.out.println("線程 A 開始 執行 wait() ");
lock.wait();
System.out.println("線程 A 結束 執行 wait()");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("線程 B 等待 獲得 鎖");
synchronized (lock) {
System.out.println("線程 B 獲得 鎖");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
System.out.println("線程 B 執行 notify()");
}
}
}).start();
}
}
執行結果:
線程 A 等待 獲得 鎖
線程 A 獲得 鎖
線程 B 等待 獲得 鎖
線程 A 開始 執行 wait()
線程 B 獲得 鎖
線程 B 執行 notify()
線程 A 結束 執行 wait()
使用時切記:必須由同一個lock對象調用wait、notify方法
- 當線程A執行wait方法時,該線程會被挂起;
- 當線程B執行notify方法時,會喚醒一個被挂起的線程A;
lock對象、線程A和線程B三者是一種什麼關系?
根據上面的案例,可以想象一個場景:
- lock對象維護了一個等待隊列list;
- 線程A中執行lock的wait方法,把線程A儲存到list中;
- 線程B中執行lock的notify方法,從等待隊列中取出線程A繼續執行;
幾個疑問
問題一: 為何wait¬ify必須要加synchronized鎖?
從實作上來說,這個synchronized鎖至關重要!
正因為這把鎖,才能讓整個wait/notify運轉起來.
當然我覺得其實通過其他的方式也可以實作類似的機制,
不過hotspot至少是完全依賴這把鎖來實作wait/notify的.
static void Sort(int [] array) {
// synchronize this operation so that some other thread can't
// manipulate the array while we are sorting it. This assumes that other
// threads also synchronize their accesses to the array.
synchronized(array) {
// now sort elements in array
}
}
synchronized代碼塊通過javap生成的位元組碼中包含monitorenter 和 monitorexit 指令
如下圖所示:
image
執行monitorenter指令可以擷取對象的monitor,
而lock.wait()方法通過調用native方法wait(0)實作,其中接口注釋中有這麼一句:
The current thread must own this object's monitor.
表示線程執行 lock.wait() 方法時,必須持有該lock對象的monitor.
問題二: 為什麼wait方法可能抛出InterruptedException異常?
這個異常大家應該都知道,當我們調用了某個線程的interrupt方法時,對應的線程會抛出這個異常;
wait方法也不希望破壞這種規則,
是以就算目前線程因為wait一直在阻塞,當某個線程希望它起來繼續執行的時候,它還是得從阻塞态恢複過來;
而wait方法被喚醒起來的時候會去檢測這個狀态,當有線程interrupt了,它就會抛出這個異常從阻塞狀态恢複過來。
這裡有兩點要注意:
- 如果被interrupt的線程隻是建立了,并沒有start,那等他start之後進入wait态之後也是不能會恢複的;
- 如果被interrupt的線程已經start了,在進入wait之前,如果有線程調用了其interrupt方法,那這個wait等于什麼都沒做,會直接跳出來,不會阻塞;
問題三: notify執行之後立馬喚醒線程嗎?
其實hotspot裡真正的實作是: 退出同步塊的時候才會去真正喚醒對應的線程; 不過這個也是個預設政策,也可以改的,在notify之後立馬喚醒相關線程。
問題四: notifyAll是怎麼實作全喚起所有線程?
或許大家立馬就能想到一個for循環就搞定了,不過在JVM裡沒實作這麼簡單,而是借助了monitorexit.
上面提到了當某個線程從wait狀态恢複出來的時候,要先擷取鎖,然後再退出同步塊;
是以notifyAll的實作是調用notify的線程在退出其同步塊的時候喚醒起最後一個進入wait狀态的線程;
然後這個線程退出同步塊的時候繼續喚醒其倒數第二個進入wait狀态的線程,依次類推.
同樣這這是一個政策的問題,JVM裡提供了挨個直接喚醒線程的參數,不過很少使用, 這裡就不提了。
問題五: wait的線程是否會影響性能?
這是個大家比較關心的話題.
wait/nofity 是通過JVM裡的 park/unpark 機制來實作的,在Linux下這種機制又是通過pthread_cond_wait/pthread_cond_signal 來實作的;
是以當線程進入到wait狀态的時候其實是會放棄cpu的,也就是說這類線程是不會占用cpu資源。
作者:鹹魚君0808
連結:https://www.jianshu.com/p/ffc0c755fd8d
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。