一、等待/通知機制的簡介
線程之間的協作:
為了完成某個任務,線程之間需要進行協作,采取的方式:中斷、互斥,以及互斥上面的線程的掛起、喚醒;如:生成者--消費者模式、或者某個動作完成,可以喚醒下一個線程、管道流已准備等等;
等待/通知機制:
等待/通知機制 是線程之間的協作一種常用的方式之一,在顯示鎖Lock 和 內置鎖synchronized都有對應的實現方式。
等待/通知機制 經典的使用方式,便是在生產者與消費者的模式中使用:
1、生產者負責生產商品,並送到倉庫中存儲;
2、消費者則從倉庫中獲取商品,享受商品;
3、倉庫的容量有限,當倉庫滿了后,生產者就會停止生產,進入等待狀態。直到消費者消耗了一部分商品,通知生產者,倉庫有空位了,生產者才會繼續生產商品;
4、當消費者的消耗速度過快,消耗光倉庫的商品時,也會停止消耗,進入等待狀態,直到生產者生產了商品,並通知喚醒消費者時,消費者才繼續消耗商品。
二、等待/通知機制 在synchronized下的實現:
在內置鎖下,等待/通知機制 是依賴於wait( )和 notify、notifyAll 來實現的,這三個方法都是繼承自根類Object中。
下面是這幾個方法的API描述,wait()方法有3個版本:
void wait():
在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。
void wait(long timeout):
超時等待,在其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量前,導致當前線程等待。如果時間當時間到來了,線程還是沒有被喚醒或中斷,那么就會自動喚醒;
void wait(long timeout, int nanos):
參數timeout的單位是毫秒,參數nanos的單位是納秒,即等待時間精確到納秒。其他特性與wait(long timeout)一樣。
void notify():
喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現做出決定時發生。線程通過調用其中一個 wait 方法,在對象的監視器上等待。被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭;例如,喚醒的線程在作為鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。
void notifyAll():
喚醒在此對象監視器上等待的所有線程。
1、調用 wait、notify、notifyAll 前,必須先獲取鎖
在調用 wait、notify、notifyAll 前,必須獲取鎖(對象監視器),而且這三個方法是由synchronized的對象鎖(對象監視器)來調用。對象監視器 中維護着兩個隊列:就緒隊列,等待隊列。調用了wait()方法就是使線程進入等待隊列,不再參與鎖的競爭,直到被喚醒才能重新進入就緒隊列,才可能獲取鎖,再次被執行。
如果在沒有獲取鎖(對象監視器)的情況下,將會拋出IllegalMonitorStateException 異常:
wait、notify、notifyAll 方法只應由作為此對象監視器的所有者的線程來調用。通過以下三種方法之一,線程可以成為此對象監視器的所有者:
- 通過執行此對象的同步實例方法。
- 通過執行在此對象上進行同步的 synchronized 語句的正文。
- 對於 Class 類型的對象,可以通過執行該類的同步靜態方法。
一次只能有一個線程擁有對象的監視器。
拋出:
IllegalMonitorStateException - 如果當前線程不是此對象監視器的所有者。
看下面的例子,消費者想要10個以上的商品,如果不夠,則等待生產者生產足夠的商品,生產者再來喚醒消費者線程。
//生產者與消費者互斥使用倉庫
public static List<String> warehouse = new LinkedList<>();
public static void main(String[] args) {
//生產者線程
Thread thread_1 = new Thread(){
@Override
public void run() {
//對象監視器為warehouse,必須先獲取這個對象監視器
synchronized ( warehouse) {
int i = 0;
//生產10個商品
while(warehouse.size()<=10){
++i;
//生產商品,添加進倉庫
warehouse.add("生產了商品goods"+i);
//當商品數量足夠時,便喚醒消費者線程
if(warehouse.size()>=10){
warehouse.notify();
//生產任務完成,跳出循環,結束運行,從而可以釋放鎖
break;
}
}
}
}
};
//消費者線程
Thread thread_2 = new Thread(){
@Override
public void run() {
synchronized (warehouse) {
try {//如果倉庫的商品數量不能滿足消費者
if(warehouse.size()<10){
//消費者進入等待隊列,等待被喚醒
warehouse.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
//消費商品
for(String goods:warehouse){
System.out.println("消費者消費了商品:"+goods);
}
}
}
};
//
thread_2.start();
thread_2.setPriority(Thread.MAX_PRIORITY);
thread_1.start();
}
運行結果:
消費者消費了商品:商品goods1
消費者消費了商品:商品goods2
消費者消費了商品:商品goods3
消費者消費了商品:商品goods4
消費者消費了商品:商品goods5
消費者消費了商品:商品goods6
消費者消費了商品:商品goods7
消費者消費了商品:商品goods8
消費者消費了商品:商品goods9
消費者消費了商品:商品goods10
2、wait()方法是可以被中斷的
如果線程在等待過程中被中斷,那么線程的等待狀態就會被打斷,即由對象監視器的等待隊列進入就緒隊列;所以,可以總結一下,線程在等待狀態被喚醒的情況:
- 被其他線程調用notify、notifyAll方法喚醒;
- 如果是超時等待,那么時間到來了,線程也會自動喚醒。
- 有中斷發生,線程的等待狀態被打斷,強制喚醒。
public static void main(String[] args) throws InterruptedException {
final String lock = "lock";
Thread thread = new Thread(){
@Override
public void run() {
synchronized (lock) {
System.out.println("我即將進入等待狀態!");
try {
//超時等待5秒
lock.wait(5000);
} catch (InterruptedException e) {
System.out.println("啊!我被中斷了");
e.printStackTrace();
}
System.out.println("我在運行中....");
}
}
};
thread.start();
//main線程睡眠1秒后,中斷線程thread
Thread.sleep(1000);
thread.interrupt();
}
運行結果:
線程thread因為中斷,還沒等待5秒,就被提前喚醒。
3、wait()是釋放鎖,notify、notifyAll 不釋放鎖
調用wait()方法是線程馬上釋放鎖,進入等待隊列,而調用notify、notifyAll 后,線程不會釋放鎖,而僅僅喚醒線程而已,要等待當前運行完后,鎖才是真的被釋放,被喚醒的線程才可以競爭獲取鎖。
final String lock = "lock";
Thread thread_1 = new Thread("thread_1"){
@Override
public void run() {
synchronized (lock) {
System.out.println(getName()+"進入等待狀態");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"被喚醒,繼續運行...");
}
}
};
thread_1.start();
Thread thread_2 = new Thread("thread_2"){
@Override
public void run() {
synchronized (lock) {
System.out.println(getName()+"在運行...");
try {
//模擬占用着鎖,運行了一秒,sleep在睡眠中是不會釋放鎖的
Thread.sleep(1000);
//在調用notifyAll方法前,線程1的狀態
System.out.println("線程"+thread_1.getName()+"被喚醒前的狀態是:"+thread_1.getState());
//喚醒在對象監視器的等待隊列的所有線程
lock.notifyAll();
//模擬占用着鎖,繼續運行5次,約為5秒
int i = 0;
while(i<5){
System.out.println("線程"+thread_1.getName()+"的狀態是:"+thread_1.getState());
sleep(1000);
i++;
}
System.out.println("線程"+getName()+"運行結束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread_2.start();
運行結果:
thread_1進入等待狀態
thread_2在運行...
線程thread_1被喚醒前的狀態是:WAITING
線程thread_1的狀態是:BLOCKED
線程thread_1的狀態是:BLOCKED
線程thread_1的狀態是:BLOCKED
線程thread_1的狀態是:BLOCKED
線程thread_1的狀態是:BLOCKED
線程thread_2運行結束!
thread_1被喚醒,繼續運行...
三、易混淆的幾個方法的區別
1、wait( )方法:
- 先釋放鎖,再進入等待狀態;
- 使當前線程暫停運行,讓出CPU,不再參與CPU的調度,直到被喚醒為止;
2、sleep()方法:
- 在睡眠過程中,不釋放鎖;
- 使當前線程暫停運行,讓出CPU,不再參與CPU的調度,直到睡眠時間到來;
3、yield( )方法:
- 不釋放鎖
- 使當前線程暫停運行,給同等優先級或高優先級的線程讓出CPU。但有可能讓出CPU失敗,因為調用yield( )方法后,當前線程由運行狀態,變成就緒狀態,會馬上參與CPU的調度,也就說可能再次被調度,當前線程依舊占用着CPU。