天天看點

張小飛的Java之路——第二十四章——多線程——wait_notify

寫在前面:

視訊是什麼東西,有看文檔精彩嗎?

視訊是什麼東西,有看文檔速度快嗎?

視訊是什麼東西,有看文檔效率高嗎?

諸小亮:下面我們聊聊——線程通信技術

張小飛:多個線程之間還可以通信嗎?

諸小亮:是的,通過調用鎖對象的wait()、notify()方法,可以實作線程通信

張小飛:那具體如何使用呢?

1. 體驗

諸小亮:首先,Object 類中有 wait()、notify() 這兩個方法

  • wait():讓目前線程進入等待狀态
    • 使用方式:鎖對象.wait(),必須放到 同步代碼塊 或 同步方法 中
  • notify():喚醒對應的正在 wait 的線程
    • 使用方式:鎖對象.notify(),必須放到 同步代碼塊 或 同步方法 中
  • 調用 wait() 和 notify() 的鎖對象必須是同一個

張小飛:大概明白,不過還需要您給出具體的代碼

諸小亮:代碼也給你準備好了,注意代碼中的注釋

class Hero implements Runnable{
    //1. 搞一個鎖對象
    public static Object lock = new Object();
    @Override
    public void run(){
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "。。。。。。。擷取鎖,然後進入等待狀态");
            try {
                //2. 讓目前線程等待,使用方式:鎖對象.wait(),而且必須放到同步代碼塊或者同步方法中
                lock.wait();
            } catch (InterruptedException e) { }
            System.out.println(Thread.currentThread().getName() + "。。。。。。。結束");
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        //3. 建立一個 yase 線程
        Thread yase = new Thread(new Hero(),"yase");
        yase.start();

        //4. 主線程休眠2秒
        Thread.sleep(2000);

        //5. 使用notify喚醒yase線程,讓他繼續執行
        synchronized (Hero.lock){
            System.out.println("主線程喚醒yase。。。。。。。");
            //使用方式:鎖對象.notify(),喚醒對應的正在等待狀态的線程,必須放到同步代碼塊或者同步方法中
            Hero.lock.notify();
        }
    }
}           

結果:

張小飛的Java之路——第二十四章——多線程——wait_notify

諸小亮:你能解釋一下這個代碼的執行過程嗎?

張小飛:當然可以

yase線程先執行,在執行 lock.wait() 代碼後,進入等待狀态

main 線程在休眠 2 秒後,執行 Hero.lock.notify(); 去喚醒 yase 線程

yase線程被喚醒後,接着執行,最後輸出:yase。。。。。。。結束           

諸小亮:不錯啊

2. 注意點

諸小亮:使用 wait、notify時,需要特别注意一些地方

張小飛:都需要注意什麼?

1. wait、notify必須放到同步代碼塊或同步方法中

諸小亮:第一,wait、notify必須放到同步代碼塊或同步方法中

張小飛:哦~,這個剛才說過,不過如果不放到它裡面呢?

諸小亮:那調用 wait 方法時就會報錯,比如:

張小飛的Java之路——第二十四章——多線程——wait_notify

結果:

張小飛的Java之路——第二十四章——多線程——wait_notify

張小飛:明白了,不過這個異常是?

諸小亮:非法的監控狀态異常,線程從啟動到死亡有不同的狀态,之後會解釋

張小飛:好的

2. 鎖對象是this

張小飛:如果鎖對象是 this 呢?

諸小亮:如果鎖對象是this,那麼:this.wait(),記住必須是:鎖對象.wait(),否則:

張小飛的Java之路——第二十四章——多線程——wait_notify

上圖,鎖對象是this,但是卻調用 lock.wait();,結果:

張小飛的Java之路——第二十四章——多線程——wait_notify

張小飛:記住了

3. 鎖對象.notify()

張小飛:既然必須是 鎖對象.wait(),那麼也肯定必須是:鎖對象.notify()

諸小亮:沒錯,否則也會報錯,比如:

張小飛的Java之路——第二十四章——多線程——wait_notify

上圖,鎖對象是Hero.class,但是卻調用Hero.lock.nodify();結果:

張小飛的Java之路——第二十四章——多線程——wait_notify

4. 調用 wait() 和 notify() 的鎖對象必須是同一個

諸小亮:還有,調用 wait() 和 notify() 的鎖對象必須是同一個,否則:

張小飛的Java之路——第二十四章——多線程——wait_notify
張小飛的Java之路——第二十四章——多線程——wait_notify

結果:

張小飛的Java之路——第二十四章——多線程——wait_notify

張小飛:看輸出,程式一直沒有結束吧

諸小亮:是的,其原因

  • Hero.class.notify();隻能喚醒鎖對象是Hero.class 且 執行了 Hero.class.wait() 的線程
  • yase線程的鎖對象是lock,執行了lock.wait(),一直在等待被人喚醒,是以程式一直不結束

5. 執行 notify 後,正在 wait 的線程并不是立即運作

張小飛:我發現一個現象

諸小亮:什麼現象?

張小飛:執行 notify 後,正在 wait 的線程并不是立即運作,需要等待執行 notify 的線程釋放鎖

張小飛的Java之路——第二十四章——多線程——wait_notify

結果:

張小飛的Java之路——第二十四章——多線程——wait_notify

諸小亮:嗯,你說的很對,這也是一個需要關注的地方

6. wait()等待時,會釋放鎖對象

張小飛:還有一個問題,執行 wait() 方法後,是不是就釋放鎖對象了?

諸小亮:不錯,隻有釋放了鎖對象,main線程才能進入 synchronized 代碼中

張小飛的Java之路——第二十四章——多線程——wait_notify

上圖可以看出,yase先執行,如果wait()不釋放鎖,那麼就無法執行Hero.lock.notify()這句代碼

7. 多個線程都在wait(),notify隻會随機的喚醒一個

張小飛:如果有多個線程都調用了 wait() 方法呢?

諸小亮:問得好,不過,你何不自己試一試呢?

張小飛:嗯嗯,好的

public static void main(String[] args) throws Exception {
    //建立 2 個線程
    Thread yase = new Thread(new Hero(),"yase");
    yase.start();
    Thread laoyase = new Thread(new Hero(),"laoyase");
    laoyase.start();
    Thread.sleep(2000);
    
    //使用方式:鎖對象.notify(),喚醒對應的正在等待狀态的線程,必須放到同步代碼塊或者同步方法中
    synchronized (Hero.lock){
        System.out.println("主線程喚醒。。。。。。。");
        Hero.lock.notify();//有多個線程都在wait(),notify隻會随機的喚醒一個
    }
}           

結果:

張小飛的Java之路——第二十四章——多線程——wait_notify

張小飛:結果出來了,yase線程結束了,但是 laoyase 這個線程還在阻塞

諸小亮:其實你多執行幾次,會發現——多個線程都在wait(),notify隻會随機的喚醒一個

張小飛:好的,我再試試

8. notifyAll()

張小飛:有沒有可以把所有都在 wait 中的線程,都喚醒呢?

諸小亮:如果想全部喚醒,可以使用notifyAll(),或者執行多次notify(),比如:

張小飛的Java之路——第二十四章——多線程——wait_notify

結果:

張小飛的Java之路——第二十四章——多線程——wait_notify

另外,執行多次notify()方法也可以,全部喚醒,比如:

張小飛的Java之路——第二十四章——多線程——wait_notify

結果:略

9. 特殊情況下的notify

諸小亮:特殊情況下的 notify 方法會喚醒所有

張小飛:這是什麼意思?

諸小亮:來,看代碼,注意代碼中的注釋

class Hero extends Thread{
    public void run(){
        //1. 注意這裡的鎖對象是this
        synchronized (this){
            //2. 執行 notify 喚醒所有
            this.notify();
            System.out.println("lock線程運作----"+this.getName());
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        //3. 建立一個Thread對象,作為鎖對象
        Hero yase = new Hero();
        yase.setName("yase");

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("線程daji開始運作。。。。。。");
                //4. 注意,這裡的鎖對象是一個線程對象
                synchronized (yase){
                    try {
                        yase.wait();
                        System.out.println("線程daji結束----"+yase.getName());//輸出lock線程的名稱
                    } catch (InterruptedException e) {
                    }
                }
            }
        }, "daji").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("線程lvbu開始運作。。。。。。");
                //4. 注意,這裡的鎖對象是一個線程對象
                synchronized (yase){
                    try {
                        yase.wait();
                        System.out.println("線程vbul結束----"+yase.getName());
                    } catch (InterruptedException e) {
                    }
                }
            }
        }, "lvbu").start();

        Thread.sleep(2000);
        //5. 啟動 yase 線程
        yase.start();
    }
}           

結果:

張小飛的Java之路——第二十四章——多線程——wait_notify

張小飛:這個代碼能否具體解釋一下?

諸小亮:當然可以

首先建立一個名為 yase 的線程對象
  然後建立了 daji、lvbu 兩個線程,這兩個線程都把 yase 對象作為鎖對象
	這兩個線程分别啟動後都執行 wait 方法,進入等待模式
  最後yase線程啟動,執行run方法時,調用 this.notify();
  結果:daji、lvbu 都被喚醒           

張小飛:這是什麼原理?

諸小亮:額。。。。,很抱歉我也沒仔細研究過,暫時記住這個現象吧

張小飛:好的

3. 生産消費模式

諸小亮:接着我們說一個——生産消費模式

張小飛:這是做什麼的?

諸小亮:生産者生産資料,消費者消費資料,先看一張圖

張小飛的Java之路——第二十四章——多線程——wait_notify

張小飛:您是先說,面包師生産面包,放到面包櫃,然後銷售員再取走面包嗎?

諸小亮:是的,這就是我們生活中的——生産消費模式

諸小亮:工作中,wait()、notify() 經常用于生産消費模式

張小飛:具體怎麼使用呢?

諸小亮:給你一個需求,生産者生産商品,如果貨架上已經有商品就不再生産,

消費者消費商品,如果貨架上沒有就通知生産者

示例:

class Goods{
    String name = "哈根達斯";

    //0:表示貨架上沒有商品,需要生産
    //1:表示貨架上已有商品,需要消費
    int count = 0;
}
//消費者
class Consumer implements Runnable{
    private Goods goods;
    Consumer(Goods goods){
        this.goods = goods;
    }
    @Override
    public void run(){
        while (true){
            synchronized (goods){
                if(goods.count == 1){
                    System.out.println(Thread.currentThread().getName()+"。。。。。。消費商品---"+goods.count);
                    goods.count = 0;//消費商品
                    goods.notify();//喚醒消費者
                    //讓自己自己等待
                    try {goods.wait();} catch (InterruptedException e) {}
                }
            }
        }
    }
}
//生産者
class Producer implements Runnable{

    private Goods goods;
    Producer(Goods goods){
        this.goods = goods;
    }
    @Override
    public void run(){
        while (true){
            synchronized (goods){
                if(goods.count == 0){
                    System.out.println(Thread.currentThread().getName()+"。。。。。。生産商品---------"+goods.count);
                    goods.count = 1;//生産商品,設定count=1
                    goods.notify();//喚醒消費者
                    //自己等待
                    try {goods.wait();} catch (InterruptedException e) {}
                }
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        Goods goods = new Goods();
        new Thread(new Producer(goods), "伊利").start();
        new Thread(new Consumer(goods), "亞瑟").start();
    }
}           

結果中,兩個線程互相喚醒,交替執行

張小飛的Java之路——第二十四章——多線程——wait_notify

諸小亮:怎麼樣,可以看懂嗎?

張小飛:可以可以,原來這就是生産消費模式

4. wait 和 sleep

張小飛:我突然先到,sleep 也是可以暫停線程的

諸小亮:不錯, wait 和 sleep 有相同點,也有差別,這也是面試常見的一個問題

張小飛:那麼它們都有什麼不同的地方呢?

諸小亮:都給你總結好了,自己看看吧

相同點:都可以暫時停止一個線程

不同點:
	sleep必須指定睡眠時間,wait可以指定也可以不指定
	sleep是 Thread 的靜态方法,wait是 Object 類的成員方法
	sleep 不釋放鎖,wait 釋放鎖
	sleep 等待一定時間後自定運作,wait需要 notify 或 notifyAll 喚醒
	wait 必須配合synchronized使用,sleep不必
	sleep 會讓線程進入 TIMED_WAITING 狀态,wait讓線程進入 WAITING 狀态(之後會詳細解釋線程狀态)           

繼續閱讀