從生產(chǎn)者消費者模型看wait,notify

一、前言

先來貼一段生產(chǎn)者,消費者模型的代碼:

public class Wait {

    public volatile static int count = 0;

    public static void main(String[] args) {


        System.out.println("開啟10個生產(chǎn)者");
        for (int i = 0; i < 100; i++) {
            new Thread(new Producer(), "線程:" + i).start();
        }
        System.out.println("開啟10個消費者");
        for (int i = 0; i < 100; i++) {
            new Thread(new Consumer(), "線程:" + i).start();
        }

    }
}

// 生產(chǎn)者
class Producer implements Runnable {
    @Override
    public void run() {
        synchronized (Wait.class) {
            System.out.println("生產(chǎn)者" + Thread.currentThread().getName() + "準(zhǔn)備生產(chǎn)");
            while (Wait.count >= 10) {
                System.out.println("隊列已滿,生產(chǎn)者" + Thread.currentThread().getName() + "停止生產(chǎn)");
                try {
                    Wait.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("生產(chǎn)者" + Thread.currentThread().getName() + "生產(chǎn)完畢");
            Wait.count++;
            System.out.println("生產(chǎn)者" + Thread.currentThread().getName() + "喚醒其他線程");
            Wait.class.notifyAll();
            System.out.println("當(dāng)前count值" + Wait.count);
        }
    }
}

// 消費者
class Consumer implements Runnable {
    @Override
    public void run() {
        synchronized (Wait.class) {
            System.out.println("消費者" + Thread.currentThread().getName() + "準(zhǔn)備消費");
            while (Wait.count <= 0) {
                System.out.println("隊列已空,消費者" + Thread.currentThread().getName() + "停止消費");
                try {
                    Wait.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("消費者" + Thread.currentThread().getName() + "消費完畢");
            Wait.count--;
            System.out.println("消費者" + Thread.currentThread().getName() + "喚醒其他線程");
            Wait.class.notifyAll();
            System.out.println("當(dāng)前count值" + Wait.count);
        }
    }
}

二、為什么wait與notify只能在synchronized代碼塊中執(zhí)行

首先我們要知道,wait 與 notify 方法是屬于 Object 的,每個對象都擁有這兩個方法,這與線程特有的方法不同。

synchronized 代碼塊使用的是對象內(nèi)置的監(jiān)視器鎖,每個對象都擁有自己的監(jiān)視器鎖,而 wait,notify 方法實際上也是在鎖對象上執(zhí)行的。只有在 synchronized 代碼塊中才使用了鎖對象,所以調(diào)用 wait,notify 才有意義。

了解一下前置知識:
JVM 會為每個鎖對象維護兩個集合,Entry Set 與 Wait Set,如果一個線程 A,持有了該鎖對象,那么對于其余的線程想要獲取該鎖對象的話,它只能進入 Entry Set,并且此時線程處于 BLOCKED 狀態(tài);如果一個線程 A 調(diào)用了鎖對象的 wait 方法,那么線程 A 會釋放鎖對象,之后進入鎖對象的 Wait Set 中,并且處于 WAITING 狀態(tài)。

當(dāng)前線程調(diào)用鎖對象的 wait 方法,首先會釋放鎖,然后進入鎖對象的 Wait Set 中;當(dāng)前線程調(diào)用鎖對象的 notify 方法,會從鎖對象的 Wait Set 中喚醒一個線程。

三、為什么生產(chǎn)者消費者要用 while 循環(huán)做判斷

以消費者為例,在調(diào)用鎖對象的 wait 方法時,使用的是 while 循環(huán),如果將 while 循環(huán)換成 if 條件會產(chǎn)生什么問題?

考慮一種情況,消費者 A 在 wait 方法停住,消費者 B 消費了最后一個資源,調(diào)用 notify 喚醒了消費者 A,消費者 A 直接從 wait 方法往下走,此時資源為空,就會導(dǎo)致出現(xiàn)問題。如果使用 while 循環(huán),那么消費者 A 被喚醒后,還是會判斷一次資源情況,就不會出現(xiàn)問題。

四、為什么要用 notifyAll 不用 notify

還是以消費者為例,如果用的是 notify,一個消費者消費了最后一個資源,然后又喚醒了一個消費者,被喚醒的消費者以為沒有資源而進入等待狀態(tài),此時所有的線程都在 wait ,也就是造成了死鎖問題。

五、能不能用 Lock 實現(xiàn)生產(chǎn)者消費者

notifyAll 會造成性能問題,可以用 Lock 鎖對象實現(xiàn)生產(chǎn)者消費者:

代碼實例:

public class PAndC {

    private volatile static int i = 0;

    private static Lock lock = new ReentrantLock();
    private static Condition producerSet = lock.newCondition();
    private static Condition consumerSet = lock.newCondition();

    public static void main(String[] args) {
        for (int j = 0; j < 5; j++) {
            new Thread(() -> {
                while(true){
                    lock.lock();
                    try {
                        if(i >= 10){
                            System.out.println(Thread.currentThread().getName() + "等待");
                            producerSet.await();
                        }
                        System.out.println(Thread.currentThread().getName() + "生產(chǎn)");
                        i ++;
                        System.out.println(Thread.currentThread().getName() + "通知其他人");
                        consumerSet.signal();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            },"生產(chǎn)者").start();
        }

        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                while(true){
                    lock.lock();
                    try {
                        if(i <= 0){
                            System.out.println(Thread.currentThread().getName() + "等待");
                            consumerSet.await();
                        }
                        System.out.println(Thread.currentThread().getName() + "消費");
                        i --;
                        System.out.println(Thread.currentThread().getName() + "通知其他人");
                        producerSet.signal();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }

            },"消費者").start();
        }

    }
}

Lock 對象可以創(chuàng)建多個等待集,所以可以指定喚醒哪個等待集中的線程,不會出現(xiàn)喚醒相同類型線程導(dǎo)致的問題。
其中,Lock 對象相當(dāng)于監(jiān)視器鎖,condition 集相當(dāng)于 Wait Set,await 方法相當(dāng)于 wait 方法,signal 方法相當(dāng)于 notify 方法。

六、總結(jié)

  1. wait notify 是屬于鎖對象的方法,操作的是鎖對象維護的集合,集合中存放的是線程;
  2. 永遠(yuǎn)要在 while 循環(huán)中調(diào)用 wait 方法;
  3. 使用 Lock 對象能獲得更好的操作;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容