【并发那些事】生产者消费者问题

Step 1. 什么是生产者消费者问题

生产者消费者问题也叫有限缓冲问题,是多线程同步的一个最最最经典的问题。这个问题描述的场景是对于一个有固定大小的缓冲区,同时共享给两个线程去使用。而这两个线程会分为两个角色,一个负责往这个缓冲区里放入必定的数据,咱们叫他生产者。另外一个负责从缓冲区里取数据,咱们叫他消费者
这里就会有两个问题,第一个问题是生产者不可能无限制的放数据去缓冲区,由于缓冲区是有大小的,当缓冲区满的时候,生产者就必须中止生产。第二个问题亦然,消费者也不可能无限制的从缓冲区去取数据,取数据的前提是缓冲区里有数据,因此当缓冲区空的时候,消费者就必须中止生产。
这两个问题看起来简单,可是在实际编码的时候仍是会有许多坑,稍不留意就会铸成大错。并且上面只是单个消费者生产者问题,实现应用中,还会遇到多生产多消费等更复杂的场景。这些问题下面会详细叙述。html

Step 2. 为何会有这个问题

经过上节的内容,咱们知道了什么是生产者消费者问题。可是为何会出现这种问题呢?
其实若是说『生产者消费者问题』,可能由于有了『问题』两个字而显得比较负面。我更喜欢称之为『生产者消费者模式』,就像咱们学的那些代码设计模式同样。他实际上是多线程状况下的一种设计模式,是某些场景下久经考验的最佳实践。
那么这种模式有哪些做用呢?
他的第一个好处是解耦。
举个外卖的例子。在没有美团、饿了么以前,确定没有如今这么多满大街跑的外卖小哥。你打电话点了一份外卖,一般都是老板本身作菜本身送。你想像一下,老板洗菜、切菜、作菜,作好以后再打包,而后拎着打包盒,骑个自行车,再满小区找地址,最后送到你的手中。这里就会出现几个问题,第一,老板挺不容易的,要会洗菜、切菜、作菜烹饪一条龙,作好以后,还要会骑车,光会骑车还不行,他还要认路,哪哪小区在哪里,哪哪栋在哪里,从哪走比较近,哪一个门口保安不让进。这样就把全部的职能都集中在了老板身上,作饭与送饭,实际上是两条事,理论上没有什么联系,可是这里若是老板切菜时,一不当心切到了手,那不光菜作不了,后面也无法送。或者送外卖的路上,为赶时间闯红灯被交警拦了下来,不光饭送不了,还回不来作下一份。这就像咱们的代码全都耦合在一块儿的后果,两个业务相互影响,一个业务出现问题另外一个也跟着出现问题,一个业务变动就带着另外一个业务变动。
咱们想一想,有了外卖小哥以后呢?老板只要关注于作菜就行了,作好给到外卖小哥。外卖小哥会送到用户手上。老板想的是怎么把菜作的更好吃,外卖小哥想的是怎么最快送达。职能清晰了,效率就更高了。这里能够把老板当成生产者,对应的外卖小哥就是消费者。
他的第二个好处就是均衡生产者与消费者的能力。
仍是举外卖的例子。有些外卖是要实时准备的,好比说作菜就是这样,用户下单后,老板马上洗菜、切菜、作菜而后打包。对于比较耗时的菜品,好比煲粥、炖汤之类的时间可能很长。而外卖小哥耗费的时间只是接到通知后来到这家店的时间。由于如今的外卖系统比较智能,通知的都是距离商户最近的外卖小哥,因此到店的时间通常比较短。这种场景下瓶颈就是商家的产能,高峰期就可能会形成排队。以下图:
image.pngjava

再严重一点就会这样git

image.png

对于这个问题的缘由咱们很清楚了,是由于生产者(商家)的产能跟不上消费者(外卖小哥)的消费(送餐)速度。由于咱们把职能分开了,因此解决问题也很清晰,那就提升生产者的产能,好比说老板能够多雇几个厨师或者再开一家分店。这样就把生产者的产能提升到与消费者的产能平衡的位置。
还有另外一种生产者比消费者快的状况,好比说一些小超市,他也有外卖服务。由于他的东西都是现成的,用户下完单后,只要按订单装好就能够了。这个时候反而是从外边过来的外卖小哥要慢的多。再或者是商品准备的时间很短,可是送餐的路途遥远,路况复杂。因此瓶颈到外卖小哥身上。github

image.png

image.png

这种状况下问题也很清晰了,消费者消耗的速度跟不上生产者的产能,那扩充消费者的数量好了。好比常常遇到的外卖转单,一个外卖小哥来不及了,转给了另外一个外卖小哥。一样也能达到生产者与消费者的产能均衡。设计模式

Step 3. 怎么去实现生产者消费者模式

好了,说完了 what 还有 why,那么咱们如今接着说怎么去实现生产者消费者模式,再也不废话直接上代码。
首先咱们写一个老板类:多线程

3.1 Boss.java (老板)

/**
 * fshows.com
 * Copyright (C) 2013-2019 All Rights Reserved.
 */
package cn.coder4j.study.example.thread;

import java.util.LinkedList;

/**
 * 老板
 * @author buhao
 * @version Boss.java, v 0.1 2019-11-09 15:09 buhao
 */
public class Boss implements Runnable {
    /**
     * 最大生产数量
     */
    public static final int MAX_NUM = 5;
    /**
     * 桌子
     */
    private LinkedList<String> tables;

    public Boss(LinkedList<String> tables) {
        this.tables = tables;
    }

    @Override
    public void run() {
        // 注意点1
        while (true){
            synchronized (this.tables){
                // 注意点2
                while (tables.size() == MAX_NUM){
                    System.out.println("通知外卖小哥取餐");
                    // 注意点3
                    this.tables.notifyAll();
                    try {
                        System.out.println("老板开始休息了");
                        this.tables.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                String goods = "牛肉面" + tables.size();
                System.out.println("老板作了一碗" + goods);
                tables.addLast(goods);
            }
        }
    }
}

而后咱们再写一个外卖小哥类,可是尴尬的是发现不知道外卖小哥英文怎么写,查了一下结果以下
image.png
这个 brother 总感受怪怪的,可是我读书少,他骗我也不知道,就用这个吧。 要是有英语大神能够留言回复一下正确怎么写。并发

3.2 TakeawayBrother.java (外卖小哥)

/**
 * fshows.com
 * Copyright (C) 2013-2019 All Rights Reserved.
 */
package cn.coder4j.study.example.thread;

import java.util.LinkedList;

/**
 * 外卖小哥
 * @author buhao
 * @version TakeawayBrother.java, v 0.1 2019-11-09 15:14 buhao
 */
public class TakeawayBrother implements Runnable {

    private LinkedList<String> tables;

    public TakeawayBrother(LinkedList<String> tables) {
        this.tables = tables;
    }

    @Override
    public void run() {
        while (true){
            synchronized (this.tables){
                while (this.tables == null || this.tables.size() == 0){
                    System.out.println("催老板赶快作外卖");
                    this.tables.notifyAll();
                    try {
                        System.out.println("一边玩手机一边等外卖");
                        this.tables.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                String goods = tables.removeFirst();
                System.out.println("外卖小哥取餐了" + goods);
            }
        }
    }
}

事件发生总归有一个地方吧,通常老板把外卖给到外卖小哥都是在店铺里,最后咱们再加一个店铺场景类吧ide

3.3 StoreContext.java (店铺)

/**
 * fshows.com
 * Copyright (C) 2013-2019 All Rights Reserved.
 */
package cn.coder4j.study.example.thread;

import java.util.LinkedList;

/**
 * 店铺场景
 * @author buhao
 * @version StoreContext.java, v 0.1 2019-11-09 15:28 buhao
 */
public class StoreContext {

    public static void main(String[] args) {
        // 先建立一张用于存放外卖的桌子
        LinkedList<String> tables = new LinkedList<>();
        // 再建立一个老板
        Boss boss = new Boss(tables);
        // 最后建立一个外卖小哥
        TakeawayBrother takeawayBrother = new TakeawayBrother(tables);
        // 建立线程对象
        Thread bossThread = new Thread(boss);
        Thread takeawayBrotherThread = new Thread(takeawayBrother);
        // 运行线程
        bossThread.start();
        takeawayBrotherThread.start();
    }
}

3.4 运行结果

老板作了一碗牛肉面0
老板作了一碗牛肉面1
老板作了一碗牛肉面2
老板作了一碗牛肉面3
老板作了一碗牛肉面4
通知外卖小哥取餐
老板开始休息了
外卖小哥取餐了牛肉面0
外卖小哥取餐了牛肉面1
外卖小哥取餐了牛肉面2
外卖小哥取餐了牛肉面3
外卖小哥取餐了牛肉面4
催老板赶快作外卖
一边玩手机一边等外卖
老板作了一碗牛肉面0
老板作了一碗牛肉面1
老板作了一碗牛肉面2
老板作了一碗牛肉面3
老板作了一碗牛肉面4
通知外卖小哥取餐
老板开始休息了
外卖小哥取餐了牛肉面0
外卖小哥取餐了牛肉面1
外卖小哥取餐了牛肉面2
外卖小哥取餐了牛肉面3
外卖小哥取餐了牛肉面4
催老板赶快作外卖
一边玩手机一边等外卖
..........

Step 4. 代码说明

首先上面的代码是一个最基本的单生产单消费的例子。若是你想要多生产多消费,那多建立几个 boss 或者 takeawayBrother 就能够了。
而后店铺场景类没什么可说的,只是基本的建立线程逻辑,若是对于线程建立不了解的,能够参考前文的【并发那些事】建立线程的三种方式。此文再也不赘述。另外观察代码,能够发现生产者与消费者的代码极为类似,只是一个存一个取。这里咱们以生产者为例子说明。
首先在 Boss 类中他有两个成员属性,一个是 MAX_NUM 一个是 tables。还记得咱们在一开头提到的『固定大小的缓冲区』吗?这里的 MAX_NUM 对应的就是『固定大小』这几个字,这里咱们设置的是 5 个。他的现实意义就是老板不可能从早到晚一刻不停的作菜,通常是在点单的时候开始作,也有一些在高峰期的时候提早作一点,可是他放菜的桌子只有那么大,放满了就不能接着作。而 tables 就对应着『缓冲区』这几个字。老板作完菜总要有一个地方先放着等外卖小哥来拿吧,缓冲区就是放菜的桌子。
而后咱们再接着看代码逻辑,我在代码中标记了几个注意点。
第一个注意点是最外面一层的 while。这个是多线程通用写法,由于不写 while 的话,一次任务结束后代码就退出了。现实业务中咱们一般想要业务一直持续的运行,因此加个 while 解决。
第二个注意点 while (tables.size() == MAX_NUM) 。这个信息量相对多一点,首先 while 的判断条件的意思是判断当前桌子上的外卖是否是已经达到上限,若是是会进入 while 代码块的内容,首先通知(notifyAll)外卖小哥能够拿外卖了,而后本身能够歇着了(wait),不然接着往下走继续作。初次接触生产消费模型的同窗,很容易出错的点就是把这里的 while 写成 if。由于这里自己也只是要判断当前缓冲区是否知足生产的条件。其实在语法与逻辑上没有问题,可是在多线程下就会出现 虚假唤醒 的问题。好比如今有两个生产者都处于调用 wait 的地方。忽然消费者线程把数据消费完了,并通知了全部生产者去生产,两个生产者都接收到消息,可是只有一个生产者拿到锁,他就去生产了,生产完后,把锁就释放了,刚刚另外一个接收到消息的生产者拿到锁就接着往下走,若是这里是 if 的话,由于都已经判断过了,不会再判断,可是明显另外一个线程已经完了任务,他如今已经不符合条件。接着往下走就会出现问题。因此当这里换成 while 后,他醒来后还会接着判断一次,不知足就接着等待,这样就避免了虚假唤醒这种问题。
第三个注意点 this.tables.notifyAll()。关于第二个问题,你们可能要说了,出现问题是由于咱们同时通知了两个生产者形成的,java 自带了一个唤醒单个线程的 notify 方法为何不用,反而用唤醒全部线程的 notifyAll 方法。这是由于 notify 唤醒线程是 随机 的,也就是说你唤醒的多是生产者也多是消费者。好比说你是生产者,你生产够了,你想唤醒消费者,可是不幸的是你唤醒了另外一个生产者,另外一个生产者一觉醒来,发现菜都作完了,就接着睡,若是生产者一直唤醒的都是生产者,那么程序就会进入 假死 状态,消费者永远都处于等待状态。post

其它

1. 项目代码

由于篇幅有限,没法贴完全部代码,如遇到问题可到github上查看源码。this

2. 参考连接

  1. 生产者消费者问题[WIKI]
  2. Java多线程14:生产者/消费者模型
  3. 一篇文章,让你完全弄懂生产者--消费者问题

image.png

image.png

相关文章
相关标签/搜索