在Java的Object類中有2個咱們不怎麼經常使用(框架中用的更多)的方法:wait()與notify()或notfiyAll(),這兩個方法主要用於多線程間的協同處理,即控制線程之間的等待、通知、切換及喚醒。html
首先了解下線程有哪幾種狀態,Java的Thread.State中定義了線程的6種狀態,分別以下:java
從操做系統層面上來說,一個進程從建立到消亡期間,最多見的進程狀態有如下幾種 新建態 : 從程序映像到進程映像的轉變,尚未加入到就緒隊列中 就緒態 : 進程運行已萬事俱備,正等待調度執行 運行態 : 進程指令正在被執行 阻塞態 : 進程正在等待一個時間操做完成,例如I/O操做 完成態 : 進程運行結束,它的資源已經被釋放,供其餘活動進程使用linux
接下來咱們分析下面的代碼git
public class WaitNotify {
public static void main(String[] args) {
Object lock = new Object();
// thread1
new Thread(() -> {
System.out.println("thread1 is ready");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
synchronized (lock) {
lock.notify();
System.out.println("thread1 is notify,but not exit synchronized");
System.out.println("thread1 is exit synchronized");
}
}).start();
// thread2
new Thread(() -> {
System.out.println("thread2 is ready");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lock) {
try {
System.out.println("thread2 is waiting");
lock.wait();
System.out.println("thread2 is awake");
} catch (InterruptedException e) {
}
}
}).start();
}
}
複製代碼
上面的代碼運行結果以下github
thread1 is ready
thread2 is ready
thread2 is waiting
thread1 is notify,but not exit synchronized
thread 1 is exit synchronized
thread2 is awake
複製代碼
看到這裏你會發現平平無奇,好像也沒什麼特殊的事情,可是若是深刻分析下,就會發現如下的問題api
針對上面的問題,咱們逐個分析下bash
若是你不加,你會獲得下面的異常多線程
Exception in thread "main" java.lang.IllegalMonitorStateException
複製代碼
在JVM源代碼中首先會檢查當前線程是否持有鎖,若是沒有持有則拋出異常oracle
其次爲何要加,也有比較普遍的討論,首先wait/notify是爲了線程間通訊的,爲了這個通訊過程不被打斷,須要保證wait/notify這個總體代碼塊的原子性,因此須要經過synchronized來加鎖。框架
wait在處理過程當中會臨時釋放同步鎖(若是不釋放其餘線程沒有機會搶),不過須要注意的是當其餘線程調用notify喚起這個線程的時候,在wait方法退出以前會從新獲取這把鎖,只有獲取了這把鎖纔會繼續執行,這也和咱們的結果相符合,輸出了thread2 is awake
, 其實想一想也容易理解,synchronized的代碼其實是被被monitorenter和monitorexit包圍起來的。當咱們調用wait的時候,會釋放鎖,調用monitorexit的時候也會釋放鎖,那麼當thread2被喚醒的時候必然從新獲取到了鎖(objectMonitor::enter)。
其實從jdk源代碼的ObjectMonitor::wait方法能夠一窺究竟,首先會放棄已經搶到的鎖(exit(self)),而放棄鎖的前提是獲取到鎖
而在notify方法中會選取一個線程得到cpu執行權,在去競爭鎖,若是沒有競爭到則會進入休眠。
若是調用的wait(200)這種代碼,那麼會在200ms後將線程從waiting set中移除並容許其從新競爭鎖,須要注意的是notify方法並不會釋放所持有的monitor
當咱們調用了某個線程的interrupt方法,對應的線程會拋出這個異常,wait方法也不但願去破壞這種規則,所以就算當前線程由於wait一直在阻塞。當某個線程但願它起來繼續執行的時候,它仍是得從阻塞態恢復過來,所以wait方法被喚醒起來的時候會去檢測這個狀態,當有線程interrupt了它的時候,它就會拋出這個異常從阻塞狀態恢復過來。
這裏有兩點要注意:
Thread thread2 = new Thread(() -> {
System.out.println("thread2 is ready");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lock) {
try {
System.out.println("thread2 is waiting");
lock.wait();
System.out.println("thread2 is awake");
} catch (InterruptedException e) {
System.out.println(e);
}
}
});
// main
thread2.start();
Thread.sleep(1000);
thread2.interrupt();
複製代碼
上面的代碼輸出結果以下
thread2 is ready
thread2 is waiting
java.lang.InterruptedException
複製代碼
hotspot真正的實現是退出同步代碼塊的時候纔會去真正喚醒對應的線程,不過這個也是個默認策略,也能夠改的,在notify以後立馬喚醒相關線程。 這個也可從jdk源代碼的objectMonitor類objectMonitor::notify方法中看到.在調用notify時的默認策略是Policy == 2(這個值是源碼中的初值,能夠經過-XX:SyncKnobs來設置)
其實對於Policy(一、二、三、4)都是將objectMonitor的ObjectWaiter集合中取出一個等待線程,放入到_EntryList(blocked線程集合,能夠參與下次搶鎖),只是放入_EntryList的策略不同,體現爲喚醒wait線程的規則不同。
對於默認策略notify在將一個等待線程放入阻塞線程集合以後就退出,由於同步塊尚未執行完monitorexit,鎖其實還未釋放,因此在打印出「thread1 is exit synchronized!」的時候,thread2線程仍是blocked狀態(由於thread1尚未退出同步塊)。
這裏能夠發現,對於不在Policy中的狀況,會直接將一個ObjectWaiter進行unpark喚醒操做,可是被喚醒的線程是否當即獲取到了鎖呢?答案是否認的。
咱們本身實現可能一個for循環就搞定了,不過在jvm裏實現沒這麼簡單,而是藉助了monitor_exit,上面我提到了當某個線程從wait狀態恢復出來的時候,要先獲取鎖,而後再退出同步塊,因此notifyAll的實現是調用notify的線程在退出其同步塊的時候喚醒起最後一個進入wait狀態的線程,而後這個線程退出同步塊的時候繼續喚醒其倒數第二個進入wait狀態的線程,依次類推,一樣這是一個策略的問題,jvm裏提供了挨個直接喚醒線程的參數,這裏要分狀況:
這個或許是你們比較關心的話題,由於關乎系統性能問題,wait/nofity是經過jvm裏的park/unpark機制來實現的,在linux下這種機制又是經過pthread_cond_wait/pthread_cond_signal來玩的,所以當線程進入到wait狀態的時候實際上是會放棄cpu的,也就是說這類線程是不會佔用cpu資源,也不會影響系統加載。
Java中每個對象均可以成爲一個監視器(Monitor), 該Monitor由一個鎖(lock), 一個等待隊列(waiting queue ), 一個入口隊列( entry queue)組成. 對於一個對象的方法, 若是沒有synchonized關鍵字, 該方法能夠被任意數量的線程,在任意時刻調用。 對於添加了synchronized關鍵字的方法,任意時刻只能被惟一的一個得到了對象實例鎖的線程調用。
進入區(Entry Set): 表示線程經過 synchronized要求得到對象鎖,若是獲取到了,則成爲擁有者,若是沒有獲取到在在進入區等待,直到其餘線程釋放鎖以後再去競爭(誰獲取到則根據)
擁有者(Owner): 表示線程獲取到了對象鎖,能夠執行synchronized包圍的代碼了
等待區(Wait Set): 表示線程調用了wait方法,此時釋放了持有的對象鎖,等待被喚醒(誰被喚醒取得監視器鎖由jvm決定)
它是一個靜態方法,通常的調用方式是Thread.sleep(2000),表示讓當前線程休眠2000ms,並不會讓出監視器,這一點須要注意。
你的關注是對我最大的支持。