首頁 > Java > java教程 > 主體

Java中如何使用wait和notify實現線程間的通信

WBOY
發布: 2023-04-22 12:01:19
轉載
806 人瀏覽過

    一. 為什麼需要執行緒通訊

    執行緒是並發並行的執行,表現出來是執行緒隨機執行,但是我們在實際應用中對執行緒的執行順序是有要求的,這就需要用到線程通訊

    線程通訊為什麼不使用優先權來解決執行緒的運行順序?

    總的優先權是由執行緒pcb中的優先權資訊和執行緒等待時間共同決定的,所以一般開發中不會依賴優先權來表示執行緒的執行順序

    看下面這樣的一個場景:麵包房的例子來描述生產者消費者模型

    有一個麵包店,裡面有麵包師傅和顧客,對應我們的生產者和消費者,而麵包房有一個庫存用來存放麵包,當庫存滿了之後就不在生產,同時消費者也在購買麵包,當庫存麵包賣完了之後,消費者必須等待新的麵包生產出來才能繼續購買

    分析:對於何時停止生產何時停止消費就需要應用到線程通信來準確的傳達生產和消費信息

    二. wait和notify方法

    wait():讓當前線程持有的對象鎖定釋放並等待

    wait(long timeout):對應的參數是線程等待的時間

    notify():喚醒使用同一個物件呼叫wait進入等待的線程,重新競爭物件鎖定

    notifyAll():如果有多個執行緒等待,notifyAll是全部喚醒,notify是隨機喚醒一個

    注意:

    這幾個方法都屬於Object類別中的方法

    必須使用在synchronized同步程式碼區塊/同步方法中

    哪個物件加鎖,就是用哪個物件wait,notify

    #調用notify後不是立即喚醒,而是等synchronized結束以後,才喚醒

    1. wait()方法

    呼叫wait方法後: 

    #讓執行目前程式碼的執行緒進行等待(執行緒放在等待佇列)

    釋放目前的鎖定

    #滿足一定條件時被喚醒,重新嘗試取得鎖定

    #wait等待結束的條件:

    其他執行緒呼叫該物件的notify方法

    wait等待時間逾時(timeout參數來指定等待時間)

    其他執行緒呼叫interrupted方法,導致wait拋出InterruptedException異常

    2. notify()方法 

    當使用wait不帶參數的方法時,喚醒執行緒等待就需要使用notify方法

    這個方法是喚醒那些等待該物件的物件鎖定的線程,使他們可以重新獲取該物件的物件鎖定 

    如果有多個線程等待,則由線程調度器隨機挑選出一個呈wait狀態的執行緒(不存在先來後到)

    在notify()方法後,目前執行緒不會馬上釋放該物件鎖,要等到執行notify()方法的執行緒將程式執行完,也就是退出同步程式碼區塊之後才會釋放物件鎖定

    3. notifyAll()方法

    該方法和notify()方法作用一樣,只是喚醒的時候,將所有等待的執行緒都喚醒

    notify()方法只是隨機喚醒一個執行緒 

    三. 使用wait和notify實現麵包房業務 

    前提說明:

    有2個麵包師傅,麵包師傅一次可以做出兩個麵包

    倉庫可以存放100個麵包

    有10個消費者,每個消費者一次購買一個麵包 

    注意:

    消費和生產是同時並發並行進行的,不是一次生產一次消費

    實作程式碼:

    public class Bakery {
        private static int total;//库存
     
        public static void main(String[] args) {
            Producer producer = new Producer();
            for(int i = 0;i < 2;i++){
                new Thread(producer,"面包师傅-"+(i-1)).start();
            }
            Consumer consumer = new Consumer();
            for(int i = 0;i < 10;i++){
                new Thread(consumer,"消费者-"+(i-1)).start();
            }
        }
        private static class Producer implements Runnable{
            private int num = 3; //生产者每次生产三个面包
            @Override
            public void run() {
                try {
                    while(true){ //一直生产
                        synchronized (Bakery.class){
                            while((total+num)>100){ //仓库满了,生产者等待
                                Bakery.class.wait();
                            }
                            //等待解除
                            total += num;
                            System.out.println(Thread.currentThread().getName()+"生产面包,库存:"+total);
                            Thread.sleep(500);
                            Bakery.class.notifyAll(); //唤醒生产
                        }
                        Thread.sleep(500);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        private static class Consumer implements Runnable{
            private int num = 1; //消费者每次消费1个面包
            @Override
            public void run() {
                try {
                    while(true){ //一直消费
                        synchronized (Bakery.class){
                            while((total-num)<0){ //仓库空了,消费者等待
                                Bakery.class.wait();
                            }
                            //解除消费者等待
                            total -= num;
                            System.out.println(Thread.currentThread().getName()+"消费面包,库存:"+total);
                            Thread.sleep(500);
                            Bakery.class.notifyAll(); //唤醒消费
                        }
                        Thread.sleep(500);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    登入後複製

    部分列印結果:

    Java中如何使用wait和notify實現線程間的通信

    四.阻塞佇列

    阻斷佇列是一個特殊的佇列,也遵循「先進先出」的原則,它是線程安全的隊列結構

    特性:典型的生產者消費者模型,一般用於做任務的解耦和消峰

    隊列滿的時候,入隊列就堵塞等待(生產),直到有其他執行緒從佇列從佇列中取走元素
    佇列空的時候,出佇列就堵塞等待(消耗),直到有其他執行緒往佇列中插入元素 

    1. 生產者消費者模式 

    生產者消費者模式就是透過一個容器來解決生產者和消費者的強耦合問題

    生產者和消費者彼此之間不直接通信,而透過阻塞隊列來進行通信,所以生產者生產完數據之後等待消費者處理,直接丟給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列取

    阻塞隊列就相當於一個緩衝區,平衡了生產者和消費者的處理能力
    阻塞隊列也能讓生產者和消費者之間解耦

    上述麵包房業務的實現就是生產者消費者模型的一個實例

    2. 標準庫中的阻塞佇列

    在Java 標準函式庫中內建了阻塞佇列, 如果我們需要在一些程式中使用阻塞佇列, 直接使用標準函式庫中的即可

    BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue

    put 方法用于阻塞式的入队列, take 用于阻塞式的出队列

    BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性

            BlockingDeque<String> queue = new LinkedBlockingDeque<>();
            queue.put("hello");
            //如果队列为空,直接出出队列就会阻塞
            String ret = queue.take();
            System.out.println(ret);
    登入後複製

    3. 阻塞队列的模拟实现

    这里使用数组实现一个循环队列来模拟阻塞队列

    当队列为空的时候,就不能取元素了,就进入wait等待,当有元素存放时,唤醒

    当队列为满的时候,就不能存元素了,就进入wait等待,当铀元素取出时,唤醒

    实现代码:

    public class MyBlockingQueue {
        //使用数组实现一个循环队列,队列里面存放的是线程要执行的任务
        private Runnable[] tasks;
        //队列中任务的数量,根据数量来判断是否可以存取
        private int count;
        private int putIndex; //存放任务位置
        private int takeIndex; //取出任务位置
     
        //有参的构造方法,表示队列容量
        public MyBlockingQueue(int size){
            tasks = new Runnable[size];
        }
     
        //存任务
        public void put(Runnable task){
            try {
                synchronized (MyBlockingQueue.class){
                    //如果队列容量满了,则存任务等待
                    while(count == tasks.length){
                        MyBlockingQueue.class.wait();
                    }
                    tasks[putIndex] = task; //将任务放入数组
                    putIndex = (putIndex+1) % tasks.length; //更新存任务位置
                    count++; //更新存放数量
                    MyBlockingQueue.class.notifyAll(); //唤醒存任务
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
     
        //取任务
        public Runnable take(){
            try {
                synchronized (MyBlockingQueue.class){
                    //如果队列任务为空,则取任务等待
                    while(count==0){
                        MyBlockingQueue.class.wait();
                    }
                    //取任务
                    Runnable task = tasks[takeIndex];
                    takeIndex = (takeIndex+1) % tasks.length; //更新取任务位置
                    count--; //更新存放数量
                    MyBlockingQueue.class.notifyAll(); //唤醒取任务
                    return task;
                }
            } catch (InterruptedException e) {
               throw new RuntimeException("存放任务出错",e);
            }
        }
    }
    登入後複製

    五. wait和sleep的区别(面试题)

    相同点:

    都可以让线程放弃执行一段时间 

    不同点:

    ☘️wait用于线程通信,让线程在等待队列中等待

    ☘️sleep让线程阻塞一段时间,阻塞在阻塞队列中

    ☘️wait需要搭配synchronized使用,sleep不用搭配

    ☘️wait是Object类的方法,sleep是Thread的静态方法

    以上是Java中如何使用wait和notify實現線程間的通信的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    相關標籤:
    來源:yisu.com
    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
    熱門教學
    更多>
    最新下載
    更多>
    網站特效
    網站源碼
    網站素材
    前端模板