#母雞下蛋實例:多線程通信生產者和消費者wait/notify和condition/await/s

簡介


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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章