等待/通知機(jī)制
前面部分介紹了Java語(yǔ)言中多線程的使用恨溜,以及方法及變量在同步情況下的處理方式,本節(jié)將介紹多個(gè)線程之間進(jìn)行通信摆寄,通過(guò)本節(jié)的學(xué)習(xí)可以了解到垃环,線程與線程之間不是獨(dú)立的個(gè)體,他們彼此之間可以互相通信和協(xié)作
不使用等待/通知機(jī)制實(shí)現(xiàn)線程間通信
創(chuàng)建項(xiàng)目异赫,在試驗(yàn)中使用sleep()結(jié)合while(true)死循環(huán)法來(lái)實(shí)現(xiàn)多個(gè)線程間通信
代碼為
import java.util.ArrayList;
import java.util.List;
class MyList{
private List list = new ArrayList();
public void add(){
list.add("秦加興");
}
public int size(){
return list.size();
}
}
//定義線程類ThreadA以及ThreadB
class ThreadA extends Thread{
private MyList list;
public ThreadA(MyList list) {
super();
this.list = list;
}
@Override
public void run() {
try {
for(int i=0;i<10;i++){
list.add();
System.out.println("添加了 "+(i+1)+" 個(gè)元素");
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
class ThreadB extends Thread{
private MyList list;
public ThreadB(MyList list) {
super();
this.list = list;
}
@Override
public void run() {
try {
System.out.println("-------"+list.size());
while(true){
if(list.size()==5){
System.out.println("==5了椅挣,線程b要退出了!");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test1 {
public static void main(String[] args) {
MyList service = new MyList();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
}
}
程序云運(yùn)行后出現(xiàn)!](http://upload-images.jianshu.io/upload_images/12188537-aec0ea58f7df4bd0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
解釋:雖然兩個(gè)線程間實(shí)現(xiàn)了通信塔拳,但有一個(gè)弊端就是鼠证,線程ThreadB.java不停的通過(guò)while語(yǔ)句輪詢機(jī)制來(lái)檢測(cè)某一個(gè)條件會(huì)很浪費(fèi)CPU資源
什么是等待/通知機(jī)制
等待/通知機(jī)制在生活中比比皆是,比如在就餐時(shí)就會(huì)出現(xiàn)如圖所示:廚師和服務(wù)員之間的交互要在“菜品傳遞臺(tái)”上靠抑,在這期間會(huì)有幾個(gè)問(wèn)題
- 廚師做完一道菜的時(shí)間不確定量九,所以廚師將菜品放在"菜品傳遞臺(tái)"上的時(shí)間也不確定。
- 服務(wù)員取到菜的時(shí)間取決于廚師颂碧,所以服務(wù)員就有“等待”(wait)的狀態(tài)荠列。
- 服務(wù)員如何能取到菜呢?這又得取決于廚師载城。廚師將菜放在“菜品傳遞臺(tái)”上肌似,其實(shí)就相當(dāng)于一種通知(notify),這是服務(wù)員才可以拿到菜并交給就餐者诉瓦。
- 在這個(gè)過(guò)程中出現(xiàn)了“等待/通知”機(jī)制锈嫩。
需要說(shuō)明一下,前面章節(jié)中多個(gè)線程之間也可以實(shí)現(xiàn)通信垦搬,原因就是多個(gè)線程共同訪問(wèn)同一個(gè)變量呼寸,但那種通信機(jī)制不是“等待/通知”,兩個(gè)線程完全是主動(dòng)式地讀取一個(gè)共享變量,在花費(fèi)讀取時(shí)間的基礎(chǔ)上猴贰,讀到的值是不是想要的对雪,并不能完全確定。所以現(xiàn)在迫切想要一種“等待/通知”機(jī)制來(lái)滿足上面的需求米绕。
等待/通知機(jī)制的實(shí)現(xiàn)
方法wait()的作用
當(dāng)前執(zhí)行代碼的線程進(jìn)行等待瑟捣,wait()方法是Object類的方法馋艺,該方法用來(lái)將當(dāng)前線程置入“預(yù)執(zhí)行隊(duì)列”中,并且在wait()所在的代碼行處停止執(zhí)行迈套,知道街道通知或被中斷為止捐祠。在調(diào)用wait()之前,線程必須獲得該對(duì)象級(jí)別鎖桑李,即只能在同步方法或同步代碼塊中調(diào)用wait()方法踱蛀。 在執(zhí)行wait()方法后,當(dāng)前線程釋放鎖贵白。 在從wait()返回前率拒,線程與其他線程競(jìng)爭(zhēng)重新獲得鎖。如果調(diào)用wait()時(shí)沒(méi)有持有適當(dāng)?shù)逆i禁荒,則拋出IllegalMonitorStateException 猬膨,它是RuntimeException 的一個(gè)子類,因此呛伴,不需要try-catch 語(yǔ)句進(jìn)行捕獲異常勃痴。
方法notify作用
也要在同步方法或同步塊中調(diào)用,即在調(diào)用前热康,線程也必須獲得該對(duì)象的對(duì)象級(jí)別鎖召耘。如果調(diào)用notify()時(shí)沒(méi)有持有適當(dāng)?shù)逆i,也會(huì)拋出IllegalMonitorStateException 褐隆。該方法用來(lái)通知那些可能等待該對(duì)象的對(duì)象鎖的其他線程污它,如果有多個(gè)線程等待,則由線程規(guī)劃器隨機(jī)挑選其中一個(gè)呈wait狀態(tài)的線程庶弃,對(duì)其發(fā)出通知notify衫贬,并使它等待獲取該對(duì)象的對(duì)象鎖。需要說(shuō)明的是歇攻,在執(zhí)行notify()方法后固惯,當(dāng)前線程不會(huì)馬上釋放該對(duì)象鎖, 呈wait狀態(tài)的線程并不能馬上獲取該對(duì)象鎖缴守,要等待執(zhí)行notify()方法的線程將程序執(zhí)行完葬毫,也就是推出synchronized代碼后,當(dāng)前線程才會(huì)釋放鎖屡穗,而成wait狀態(tài)所在的線程才可以獲取該對(duì)象鎖贴捡。當(dāng)?shù)谝粋€(gè)獲得了該對(duì)象鎖的wait線程運(yùn)行完畢之后,它會(huì)釋放掉該對(duì)象鎖村砂,此時(shí)如果該對(duì)象沒(méi)有再次使用notify語(yǔ)句烂斋,則即便該對(duì)象已經(jīng)空閑,其他wait狀態(tài)等待的線程由于咩有得到該對(duì)象的通知,還會(huì)繼續(xù)阻塞在wait狀態(tài)汛骂,知道這個(gè)對(duì)象發(fā)出一個(gè)notify或notifyAll罕模。
- 用一句話總結(jié)一下wait和notify
wait使線程停止運(yùn)行,而notify使停止的線程繼續(xù)運(yùn)行帘瞭。
wait()和notify()的簡(jiǎn)單使用
package three;
/**
*輸出:
開(kāi)始 wait time=1493298291380
開(kāi)始 notify time=1493298294382
結(jié)束 notify time=1493298294383
結(jié)束 wait time=1493298294384
* @author jiaxing
*
*/
//定義兩個(gè)定義線程
class MyThread1 extends Thread{
private Object lock;
public MyThread1(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
super.run();
//在線程塊內(nèi)執(zhí)行wait()方法
try {
synchronized (lock) {
System.out.println("開(kāi)始 wait time="+System.currentTimeMillis());
lock.wait();
System.out.println("結(jié)束 wait time="+System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread2 extends Thread{
private Object lock;
public MyThread2(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
super.run();
//在線程塊內(nèi)執(zhí)行notify()方法
synchronized (lock) {
System.out.println("開(kāi)始 notify time="+System.currentTimeMillis());
lock.notify();
System.out.println("結(jié)束 notify time="+System.currentTimeMillis());
}
}
}
public class Test2 {
public static void main(String[] args) {
try {
Object lock = new Object();
//啟動(dòng)兩個(gè)線程
MyThread1 t1 = new MyThread1(lock);
t1.start();
Thread.sleep(3000);
MyThread2 t2 = new MyThread2(lock);
t2.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
從控制臺(tái)中可以看出3秒后線程被notify通知喚醒
如何使用wait()和notify()來(lái)實(shí)現(xiàn)前面size()等于5呢
- 看下面例子:
package three;
import java.util.ArrayList;
import java.util.List;
/***
* 輸出:
wait begin 1493298319090
添加了1個(gè)元素
添加了2個(gè)元素
添加了3個(gè)元素
添加了4個(gè)元素
已發(fā)出通知淑掌!
添加了5個(gè)元素
添加了6個(gè)元素
添加了7個(gè)元素
添加了8個(gè)元素
添加了9個(gè)元素
添加了10個(gè)元素
wait end 1493298329143
*解釋:日志信息中wait end 在最后輸出,這也說(shuō)明notify()方法執(zhí)行后并不立刻釋放鎖蝶念。這個(gè)知識(shí)點(diǎn)在后面進(jìn)行補(bǔ)充介紹抛腕。
* @author jiaxing
*
*/
class MyList3{
private static List list = new ArrayList();
public static void add(){
list.add("anything");
}
public static int size(){
return list.size();
}
}
class ThreadA3 extends Thread{
private Object lock;
public ThreadA3(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
super.run();
try {
synchronized(lock){
if(MyList3.size()!=5){
System.out.println("wait begin "+System.currentTimeMillis());
lock.wait();
System.out.println("wait end "+System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadB3 extends Thread{
private Object lock;
public ThreadB3(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
super.run();
try {
synchronized (lock) {
for(int i=0;i<10;i++){
MyList3.add();
if(MyList3.size()==5){
lock.notify();
System.out.println("已發(fā)出通知!");
}
System.out.println("添加了"+(i+1)+"個(gè)元素");
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test3 {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA3 a = new ThreadA3(lock);
a.start();
Thread.sleep(50);
ThreadB3 b = new ThreadB3(lock);
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
關(guān)鍵字synchronized 可以將任何一個(gè)Obejct 對(duì)象作為同步對(duì)象看待祸轮,而java為每個(gè)Object 都實(shí)現(xiàn)了wait() 和notify() 方法,他們必須在被synchronized 同步的Obejct 的臨界區(qū)內(nèi)侥钳。通過(guò)調(diào)用wait() 方法可以使處于臨界內(nèi)的線程進(jìn)入等待狀態(tài)适袜,同時(shí)釋放被同步對(duì)象的鎖。而notify操作可以喚醒一個(gè)因調(diào)用了wait 操作而處于阻塞狀態(tài)中的線程舷夺,使其進(jìn)入就緒狀態(tài)苦酱。被重新喚醒的線程會(huì)視圖重新獲得臨界區(qū)的控制權(quán),也就是鎖给猾,并繼續(xù)執(zhí)行臨界區(qū)內(nèi)wait 之后的代碼疫萤。如果發(fā)出notify 操作時(shí)沒(méi)有處于阻塞狀態(tài)中的線程,那么該命令會(huì)被忽略敢伸。
wait() 方法可以調(diào)用該方法的線程釋放共享資源的鎖扯饶,然后從運(yùn)行狀態(tài)推出,進(jìn)入等待隊(duì)列池颈,知道被再次喚醒尾序。
notify() 方法可以隨機(jī)喚醒等待隊(duì)列中等待同一共享資源的“一個(gè)”線程,并使該線程退出等待隊(duì)列躯砰,進(jìn)入可運(yùn)行狀態(tài)每币,也就是notify() 方法僅通知“一個(gè)”線程。
notifyAll() 方法可以使所有正在等待隊(duì)列中等待同一共享資源的“全部”線程從等待狀態(tài)退出琢歇,進(jìn)入可運(yùn)行狀態(tài)兰怠。此時(shí),優(yōu)先級(jí)最高的那個(gè)線程最先執(zhí)行李茫,但也有可能是隨機(jī)執(zhí)行揭保,因?yàn)檫@要取決于JVM虛擬機(jī)的實(shí)現(xiàn)。
新創(chuàng)建一個(gè)新的線程對(duì)象后,再調(diào)用它的start() 方法,系統(tǒng)會(huì)為此線程分配CPU 資
源塔次,使其處于Runnable(可運(yùn)行) 狀態(tài)方篮,這是一個(gè)準(zhǔn)備運(yùn)行的階段。如果線程搶占到CPU 資
源励负,此線程就處于Running(運(yùn)行) 狀態(tài)藕溅。Runnable 狀態(tài)和Running 狀態(tài)可相互切換,因?yàn)橛锌赡芫€程運(yùn)行一段時(shí)間后继榆,有其
他高優(yōu)先級(jí)的線程搶占了CPU 資源巾表,這時(shí)此線程就從Running 狀態(tài)變成Runnable 狀態(tài)。
線程進(jìn)入Runnable狀態(tài)大致分為如下5中情況:
- 調(diào)用sleep() 方法后經(jīng)過(guò)的時(shí)間超過(guò)了指定的休眠時(shí)間略吨。
- 線程調(diào)用的阻塞IO 已經(jīng)返回集币,阻塞方法執(zhí)行完畢。
- 線程成功地獲得了試圖同步的監(jiān)視器翠忠。
- 線程正在等待某個(gè)通知鞠苟,其他線程發(fā)出了通知。
- 處于掛起狀態(tài)的線程調(diào)用了resume 恢復(fù)方法秽之。
-
Blocked 是阻塞的意思当娱,例如遇到了一個(gè)IO 操作,此時(shí)CPU 處于空閑狀態(tài)考榨,可能會(huì)
轉(zhuǎn)而把CPU 時(shí)間片分配給其他線程跨细,這時(shí)也可以稱為"暫停"狀態(tài)。Blocked 狀態(tài)結(jié)束后‘
進(jìn)入Runnable 狀態(tài)河质, 等待系統(tǒng)重新分配資掘冀惭。
出現(xiàn)阻塞的情況大體分為如下5種:
- 線程調(diào)用sleep 方法, 主動(dòng)放棄占用的處理器資源掀鹅。
- 線程調(diào)用了阻塞式IO 方法云头,在該方法返回前,該線程被阻塞淫半。
- 線程試圖獲得一個(gè)同步監(jiān)視器溃槐,但該同步監(jiān)視器正被其他線程所持有。
- 線程等待某個(gè)通知科吭。
- 程序調(diào)用了suspend 方法將該線程掛起昏滴。此方法容易導(dǎo)致死鎖,盡量避免使用該方法对人。
- run() 方法運(yùn)行結(jié)束后進(jìn)入銷毀階段谣殊, 整個(gè)線程執(zhí)行完畢。
每個(gè)對(duì)象都有兩個(gè)隊(duì)列牺弄,一個(gè)是就緒隊(duì)列姻几,一個(gè)是阻塞隊(duì)列。就緒隊(duì)列存儲(chǔ)了將要獲得鎖的線程,阻塞隊(duì)列存儲(chǔ)了被阻塞的線程蛇捌。一個(gè)線程被喚醒后抚恒,才會(huì)進(jìn)入就緒隊(duì)列,等待CPU的調(diào)度络拌;反之俭驮,一個(gè)線程被wait后,就會(huì)進(jìn)入阻塞隊(duì)列春贸,等待下一次被喚醒混萝。
方法wait()鎖釋放與notify()鎖不釋放
當(dāng)方法wait()被釋放后,鎖被自動(dòng)釋放萍恕,但執(zhí)行完notify()方法逸嘀,鎖卻不自動(dòng)釋放。
- 看下面的例子展示:
package three;
/***
* 輸出結(jié)果:
begin wait()
begin wait()
* @author jiaxing
*
*/
class Service{
public void testMethod(Object lock){
try {
synchronized (lock) {
System.out.println("begin wait()");
//Thread.sleep(4000); 同步效果
lock.wait();
System.out.println(" end wait()");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA4 extends Thread{
private Object lock;
public ThreadA4(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
super.run();
//實(shí)例化對(duì)象
Service service = new Service();
service.testMethod(lock);
}
}
class ThreadB4 extends Thread{
private Object lock;
public ThreadB4(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
super.run();
Service service = new Service();
service.testMethod(lock);
}
}
public class Test4 {
public static void main(String[] args) {
Object lock = new Object();
//傳入一個(gè)對(duì)象
ThreadA4 a = new ThreadA4(lock);
a.start();
ThreadB4 b= new ThreadB4(lock);
b.start();
}
}
還有一個(gè)實(shí)驗(yàn):方法notify()被執(zhí)行后允粤,不釋放鎖 崭倘,下面看代碼展示:
package three;
/***
* 輸出
begin wait() ThreadName=Thread-0
begin notify() ThreadName=Thread-2
end notify() ThreadName=Thread-2
begin notify() ThreadName=Thread-1
end notify() ThreadName=Thread-1
end wait() ThreadName=Thread-0
解釋:
結(jié)果顯示:必須執(zhí)行完notify()方法所在的同步synchronized代碼塊后才釋放鎖
* @author jiaxing
*
*/
class Service5{
//多個(gè)通知一個(gè)等待
public void testMethod(Object lock){
//等待方法
try {
synchronized (lock) {
System.out.println("begin wait() ThreadName="+Thread.currentThread().getName());
lock.wait();
System.out.println(" end wait() ThreadName="+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知notify方法
public void synNotifyMethod(Object lock){
try {
synchronized (lock) {
System.out.println("begin notify() ThreadName="+Thread.currentThread().getName());
lock.notify();
Thread.sleep(5000);
System.out.println(" end notify() ThreadName="+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//自定義Thread方法
class ThreadA5 extends Thread{
private Object lock;
public ThreadA5(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
super.run();
Service5 service = new Service5();
service.testMethod(lock);
}
}
//自定義Thread方法調(diào)用notify方法
class NotifyThread extends Thread{
private Object lock;
public NotifyThread(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
super.run();
//實(shí)例化后調(diào)用notify方法
Service5 service = new Service5();
service.synNotifyMethod(lock);
}
}
//自定義SynNotifyThread方法調(diào)用notify方法
class SynNotifyThread extends Thread{
private Object lock;
public SynNotifyThread(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
super.run();
//實(shí)例化后調(diào)用notify方法
Service5 service = new Service5();
service.synNotifyMethod(lock);
}
}
//測(cè)試類
public class Test5 {
public static void main(String[] args) {
Object lock = new Object();
//線程a啟動(dòng) wait方法
ThreadA5 a = new ThreadA5(lock);
a.start();
//線程b啟動(dòng) notify方法
NotifyThread b = new NotifyThread(lock);
b.start();
//線程c啟動(dòng) notify方法
SynNotifyThread c = new SynNotifyThread(lock);
c.start();
}
}
當(dāng)interrupt方法遇到wait方法
- 當(dāng)線程呈wait() 狀態(tài)時(shí),調(diào)用線程對(duì)象的interrupt() 方法會(huì)出現(xiàn)InterruptedException 異常**维哈。
- 創(chuàng)建項(xiàng)目后绳姨,測(cè)試部分代碼如下:則當(dāng)程序運(yùn)行后登澜,停止wait狀態(tài)下的線程出現(xiàn)異常
public void main (String[] args){
try {
Obejct lock = new Object();
ThreadA a = new ThreadA(lock);
a.start();
Thread.sleep(5000);
a.interrupted();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
通過(guò)上面的幾個(gè)實(shí)驗(yàn)可以總結(jié)如下3點(diǎn):
- 執(zhí)行完同步代碼塊就會(huì)釋放對(duì)象的鎖阔挠。
- 在執(zhí)行同步代碼塊的過(guò)程中,遇到異常而導(dǎo)致線程終止脑蠕,鎖也會(huì)被釋放购撼。
- 在執(zhí)行同步代碼塊的過(guò)程中,執(zhí)行了鎖所屬對(duì)象的wait() 方法谴仙,這個(gè)線程會(huì)釋放對(duì)象鎖迂求,而此線程對(duì)象會(huì)進(jìn)入線程等待池中,等待被喚醒晃跺。
只通知一個(gè)線程
- 調(diào)用notify() 一次只隨機(jī) 通知一個(gè)線程揩局;
- 當(dāng)多次調(diào)用notify()方法時(shí),會(huì)隨機(jī)將等待wait 狀態(tài)的線程進(jìn)行喚醒掀虎。
喚醒所有線程-->notifyAll()方法
- 只需將之前的代碼中的notify()方法改寫(xiě)成notifyAll()即可凌盯。
方法wait(long)的使用
- 帶一個(gè)參數(shù)的wait(long)方法的功能是等待某一時(shí)間內(nèi)是否有線程對(duì)鎖進(jìn)行喚醒,如果超過(guò)這個(gè)時(shí)間則自動(dòng)喚醒烹玉。舉例略驰怎。
通知過(guò)早
- 如果通知過(guò)早,則會(huì)打亂程序正常的運(yùn)行邏輯二打。也即:notify()如果在wait()之前執(zhí)行時(shí)县忌,則會(huì)打亂正常的運(yùn)行邏輯。-->可能會(huì)導(dǎo)致wait()不能被執(zhí)行。