母雞下蛋例項:多執行緒通訊生產者和消費者wait/notify和condition/await/signal條件佇列

叫練發表於2020-12-31

簡介


多執行緒通訊一直是高頻面試考點,有些面試官可能要求現場手寫生產者/消費者程式碼來考察多執行緒的功底,今天我們以實際生活中母雞下蛋案例用程式碼剖析下實現過程。母雞在雞窩下蛋了,叫練從雞窩裡把雞蛋拿出來這個過程,母雞在雞窩下蛋,是生產者,叫練撿出雞蛋,叫練是消費者,一進一出就是執行緒中的生產者和消費者模型了,雞窩是放雞蛋容器。現實中還有很多這樣的案例,如醫院叫號。下面我們畫個圖表示下。
image.png

一對一生產和消費:一隻母雞和叫練


wait/notify

package com.duyang.thread.basic.waitLock.demo;

import java.util.ArrayList;
import java.util.List;

/**
 * @author :jiaolian
 * @date :Created in 2020-12-30 16:18
 * @description:母雞下蛋:一對一生產者和消費者
 * @modified By:
 * 公眾號:叫練
 */
public class SingleNotifyWait {

    //裝雞蛋的容器
    private static class EggsList {
        private static final List<String> LIST = new ArrayList();
    }

    //生產者:母雞實體類
    private static class HEN {
        private String name;

        public HEN(String name) {
            this.name = name;
        }

        //下蛋
        public void proEggs() throws InterruptedException {
            synchronized (EggsList.class) {
                if (EggsList.LIST.size() == 1) {
                    EggsList.class.wait();
                }
                //容器新增一個蛋
                EggsList.LIST.add("1");
                //雞下蛋需要休息才能繼續產蛋
                Thread.sleep(1000);
                System.out.println(name+":下了一個雞蛋!");
                //通知叫練撿蛋
                EggsList.class.notify();
            }
        }
    }

    //人物件
    private static class Person {
        private String name;

        public Person(String name) {
            this.name = name;
        }
        //取蛋
        public void getEggs() throws InterruptedException {
            synchronized (EggsList.class) {
                if (EggsList.LIST.size() == 0) {
                    EggsList.class.wait();
                }
                Thread.sleep(500);
                EggsList.LIST.remove(0);
                System.out.println(name+":從容器中撿出一個雞蛋");
                //通知叫練撿蛋
                EggsList.class.notify();
            }
        }
    }

    public static void main(String[] args) {
        //創造一個人和一隻雞
        HEN hen = new HEN("小黑");
        Person person = new Person("叫練");
        //建立執行緒執行下蛋和撿蛋的過程;
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    hen.proEggs();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        //叫練撿雞蛋的過程!
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    person.getEggs();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

如上面程式碼,我們定義EggsList類來裝雞蛋,HEN類表示母雞,Person類表示人。在主函式中建立母雞物件“小黑”,人物件“叫練”, 建立兩個執行緒分別執行下蛋和撿蛋的過程。程式碼中定義雞窩中最多隻能裝一個雞蛋(當然可以定義多個)。詳細過程:“小黑”母雞執行緒和“叫練”執行緒執行緒競爭鎖,如果“小黑”母雞執行緒先獲取鎖,發現EggsList雞蛋的個數大於0,表示有雞蛋,那就呼叫wait等待並釋放鎖給“叫練”執行緒,如果沒有雞蛋,就呼叫EggsList.LIST.add("1")表示生產了一個雞蛋並通知“叫練”來取雞蛋並釋放鎖讓“叫練”執行緒獲取鎖。“叫練”執行緒呼叫getEggs()方法獲取鎖後發現,如果雞窩中並沒有雞蛋就呼叫wait等待並釋放鎖通知“小黑”執行緒獲取鎖去下蛋,如果有雞蛋,說明“小黑”已經下蛋了,就把雞蛋取走,因為雞窩沒有雞蛋了,所以最後也要通知呼叫notify()方法通知“小黑”去下蛋,我們觀察程式的執行結果如下圖。兩個執行緒是死迴圈程式會一直執行下去,下蛋和撿蛋的過程中用到的鎖的是EggsList類的class,“小黑”和“叫練”競爭的都是統一把鎖,所以這個是同步的。這就是母雞“小黑”和“叫練”溝通的過程。
image.png
神馬???雞和人能溝通!!
image.png

Lock條件佇列

package com.duyang.thread.basic.waitLock.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author :jiaolian
 * @date :Created in 2020-12-30 16:18
 * @description:母雞下蛋:一對一生產者和消費者 條件佇列
 * @modified By:
 * 公眾號:叫練
 */
public class SingleCondition {

    private static Lock lock = new ReentrantLock();
    //條件佇列
    private static Condition condition = lock.newCondition();

    //裝雞蛋的容器
    private static class EggsList {
        private static final List<String> LIST = new ArrayList();
    }

    //生產者:母雞實體類
    private static class HEN {
        private String name;

        public HEN(String name) {
            this.name = name;
        }

        //下蛋
        public void proEggs() {
            try {
                lock.lock();
                if (EggsList.LIST.size() == 1) {
                    condition.await();
                }
                //容器新增一個蛋
                EggsList.LIST.add("1");
                //雞下蛋需要休息才能繼續產蛋
                Thread.sleep(1000);
                System.out.println(name+":下了一個雞蛋!");
                //通知叫練撿蛋
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    //人物件
    private static class Person {
        private String name;

        public Person(String name) {
            this.name = name;
        }
        //取蛋
        public void getEggs() {
            try {
                lock.lock();
                if (EggsList.LIST.size() == 0) {
                    condition.await();
                }
                Thread.sleep(500);
                EggsList.LIST.remove(0);
                System.out.println(name+":從容器中撿出一個雞蛋");
                //通知叫練撿蛋
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        //創造一個人和一隻雞
        HEN hen = new HEN("小黑");
        Person person = new Person("叫練");
        //建立執行緒執行下蛋和撿蛋的過程;
        new Thread(()->{
            for (int i=0; i<Integer.MAX_VALUE;i++) {
                hen.proEggs();
            }
        }).start();
        //叫練撿雞蛋的過程!
        new Thread(()->{
            for (int i=0; i<Integer.MAX_VALUE;i++) {
                person.getEggs();
            }
        }).start();
    }
}

如上面程式碼,只是將synchronized換成了Lock,程式執行的結果和上面的一致,wait/notify換成了AQS的條件佇列Condition來控制執行緒之間的通訊。Lock需要手動加鎖lock.lock(),解鎖lock.unlock()的步驟放在finally程式碼塊保證鎖始終能被釋放。await底層是unsafe.park(false,0)呼叫C++程式碼實現。

多對多生產和消費:2只母雞和叫練/叫練媳婦


wait/notifyAll

package com.duyang.thread.basic.waitLock.demo;

import java.util.ArrayList;
import java.util.List;

/**
 * @author :jiaolian
 * @date :Created in 2020-12-30 16:18
 * @description:母雞下蛋:多對多生產者和消費者
 * @modified By:
 * 公眾號:叫練
 */
public class MultNotifyWait {

    //裝雞蛋的容器
    private static class EggsList {
        private static final List<String> LIST = new ArrayList();
    }

    //生產者:母雞實體類
    private static class HEN {
        private String name;

        public HEN(String name) {
            this.name = name;
        }

        //下蛋
        public void proEggs() throws InterruptedException {
            synchronized (EggsList.class) {
                while (EggsList.LIST.size() >= 10) {
                    EggsList.class.wait();
                }
                //容器新增一個蛋
                EggsList.LIST.add("1");
                //雞下蛋需要休息才能繼續產蛋
                Thread.sleep(1000);
                System.out.println(name+":下了一個雞蛋!共有"+EggsList.LIST.size()+"個蛋");
                //通知叫練撿蛋
                EggsList.class.notify();
            }
        }
    }

    //人物件
    private static class Person {
        private String name;

        public Person(String name) {
            this.name = name;
        }
        //取蛋
        public void getEggs() throws InterruptedException {
            synchronized (EggsList.class) {
                while (EggsList.LIST.size() == 0) {
                    EggsList.class.wait();
                }
                Thread.sleep(500);
                EggsList.LIST.remove(0);
                System.out.println(name+":從容器中撿出一個雞蛋!還剩"+EggsList.LIST.size()+"個蛋");
                //通知叫練撿蛋
                EggsList.class.notify();
            }
        }
    }

    public static void main(String[] args) {
        //創造一個人和一隻雞
        HEN hen1 = new HEN("小黑");
        HEN hen2 = new HEN("小黃");
        Person jiaolian = new Person("叫練");
        Person wife = new Person("叫練媳婦");
        //建立執行緒執行下蛋和撿蛋的過程;
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    hen1.proEggs();
                    Thread.sleep(50);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    hen2.proEggs();
                    Thread.sleep(50);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        //叫練撿雞蛋的執行緒!
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    jiaolian.getEggs();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        //叫練媳婦撿雞蛋的執行緒!
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    wife.getEggs();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

如上面程式碼,參照一對一生產和消費中wait/notify程式碼做了一些修改,建立了兩個母雞執行緒“小黑”,“小黃”,兩個撿雞蛋的執行緒“叫練”,“叫練媳婦”,執行結果是同步的,實現了多對多的生產和消費,如下圖所示。有如下幾點需要注意的地方:

  1. 雞窩中能容納最大的雞蛋是10個。
  2. 下蛋proEggs()方法中判斷雞蛋數量是否大於等於10個使用的是while迴圈,wait收到通知,喚醒當前執行緒,需要重新判斷一次,避免程式出現邏輯問題,這裡不能用if,如果用if,程式可能出現EggsList有超過10以上雞蛋的情況。這是這道程式中容易出現錯誤的地方,也是經常會被問到的點,值得重點探究下。
  3. 多對多的生產者和消費者。

image.png

Lock條件佇列


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author :jiaolian
 * @date :Created in 2020-12-30 16:18
 * @description:母雞下蛋:多對多生產者和消費者 條件佇列
 * @modified By:
 * 公眾號:叫練
 */
public class MultCondition {

    private static Lock lock = new ReentrantLock();
    //條件佇列
    private static Condition condition = lock.newCondition();

    //裝雞蛋的容器
    private static class EggsList {
        private static final List<String> LIST = new ArrayList();
    }

    //生產者:母雞實體類
    private static class HEN {
        private String name;

        public HEN(String name) {
            this.name = name;
        }

        //下蛋
        public void proEggs() {
            try {
                lock.lock();
                while (EggsList.LIST.size() >= 10) {
                    condition.await();
                }
                //容器新增一個蛋
                EggsList.LIST.add("1");
                //雞下蛋需要休息才能繼續產蛋
                Thread.sleep(1000);
                System.out.println(name+":下了一個雞蛋!共有"+ EggsList.LIST.size()+"個蛋");
                //通知叫練/叫練媳婦撿蛋
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    //人物件
    private static class Person {
        private String name;

        public Person(String name) {
            this.name = name;
        }
        //取蛋
        public void getEggs() throws InterruptedException {
            try {
                lock.lock();
                while (EggsList.LIST.size() == 0) {
                    condition.await();
                }
                Thread.sleep(500);
                EggsList.LIST.remove(0);
                System.out.println(name+":從容器中撿出一個雞蛋!還剩"+ EggsList.LIST.size()+"個蛋");
                //通知叫練撿蛋
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        //創造一個人和一隻雞
        HEN hen1 = new HEN("小黑");
        HEN hen2 = new HEN("小黃");
        Person jiaolian = new Person("叫練");
        Person wife = new Person("叫練媳婦");
        //建立執行緒執行下蛋和撿蛋的過程;
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    hen1.proEggs();
                    Thread.sleep(50);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    hen2.proEggs();
                    Thread.sleep(50);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        //叫練撿雞蛋的執行緒!
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    jiaolian.getEggs();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        //叫練媳婦撿雞蛋的執行緒!
        new Thread(()->{
            try {
                for (int i=0; i<Integer.MAX_VALUE;i++) {
                    wife.getEggs();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

如上面程式碼,只是將synchronized換成了Lock,程式執行的結果和上面的一致,下面我們比較下Lock和synchronized的異同。這個問題也是面試中會經常問到的!

Lock和synchronized比較


Lock和synchronized都能讓多執行緒同步。主要異同點表現如下!

  1. 鎖性質:Lock樂觀鎖是非阻塞的,底層是依賴cas+volatile實現,synchronized悲觀鎖是阻塞的,需要上下文切換。實現思想不一樣。
  2. 功能細節上:Lock需要手動加解鎖,synchronized自動加解鎖。Lock還提供顆粒度更細的功能,比如tryLock等。
  3. 執行緒通訊:Lock提供Condition條件佇列,一把鎖可以對應多個條件佇列,對執行緒控制更細膩。synchronized只能對應一個wait/notify。

主要就這些吧,如果對synchronized,volatile,cas關鍵字不太瞭解的童鞋,可以看看我之前的文章,有很詳細的案例和說明。

總結


今天用生活中的例子轉化成程式碼,實現了兩種多執行緒中消費者/生產者模式,給您的建議就是需要把程式碼敲一遍,如果認真執行了一遍程式碼應該能看明白,喜歡的請點贊加關注哦。我是叫練【公眾號】,邊叫邊練。
image.png

相關文章