【Java】生产者和消费者(等待唤醒机制)

一、思路分析

1)引入

生产者和消费者 也叫做 等待唤醒机制

生产者消费者模式 是一个十分经典的多线程协作的模式,学习完了 等待唤醒机制,会让我们对多线程的运行有更加深刻的理解。

之前我们学习过,线程的执行是有随机性的,所以如果有两条线程正在执行,它的结果有可能是这样的,完全随机。

image-20240506204309069

但是我们现在学习的 等待唤醒机制,这个机制就要打破随机的规则。

它会让两个线程轮流执行,你一次我一次。这就是在 等待唤醒机制 中多线程的运行结果。

image-20240506204406248

其中的一条线程我们会将它叫做 生产者,负责 生产数据

另一条线程我们会叫做 消费者,负责 消费数据

由于这个过程非常的复杂,我们先将一个小故事,用故事去理解复杂的代码逻辑。

假设现在有两个人,一个是吃货,一个是厨师。

吃货负责吃,所以它是消费者;厨师负责做,所以它是生产者。

但是光有它们两个还不够,还需要有第三者桌子,因为在默认情况下,线程的执行是具有随机性的,我们需要有一个东西去控制线程的执行,现在我们就用桌子去控制。

image-20240506204847961

假设现在桌子上有一碗面条,如果有面条,就是吃货执行,负责吃。

image-20240506204951698

但是如果桌子上没有面条,那就是厨师去执行,它负责做面条。

image-20240506205024343

我们先来看看理想情况是怎么样的。

理想情况下,就是一开始是厨师先抢到了CPU的执行权,此时桌子上是没有面条的,所以厨师在一开始就要做一碗面条。

做完了后将买那条放在桌子上,再让吃货线程来吃,这就是理想的情况。

相当于就是厨师做一个,吃货吃一个。

但是程序程序没有这么听话,它不能按照我们自己想的去执行,线程的执行是具有随机性的。

所以我们需要将各种各样的情况给考虑到。

但是它出现的情况没有大家想的那么复杂,只有两种情况。

我们先来分析第一种:消费者等待。


2)情况一:消费者等待

假设在一开始,不是厨师先抢到CPU的执行权,而是吃货先抢到CPU的执行权,这个时候桌子上还没有东西的,总不能啃桌子吧,所以它只能先等着,代码中叫做 wait

一旦它等了,CPU的执行权一定会被厨师抢到,厨师就会看下桌子上有没有面条,现在没有,那就开始做,做一碗面条放在桌子上。

做完后还没完,吃货还在等着,因此厨师需要含吃货去吃,这个动作我们也叫作 唤醒,英文:notify

image-20240506210208638

吃货一旦被唤醒后就开始。

image-20240506210245533

以上就是第一种消费者等待的情况。

主要核心思想其实就是看桌子,桌子上如果没有面条,消费者就会等待。

消费者和生产者执行过程如下

image-20240506210403572

3)情况二:生产者等待

在一开始的时候是厨师抢到了CPU的执行权,此时桌子上是没有事物的,此时厨师就会执行我们刚刚分析到的三步:制作食物、把事物放在桌子上、叫醒等待的消费者开吃。

但是现在没有人在等,这个其实也没有什么太大关系,就是喊一下的事情,代码是不会受到任何影响的。

image-20240506210832037

这个时候,就不能是左边的吃货去抢到CPU的执行权了,如果是吃货抢到了CPU的执行权,此时就变成理想情况了。

而我们现在说的是生产者等待的情况,因此下一次还是厨师抢到CPU的执行权。

此时厨师抢到CPU执行权后,她就不能做面条了,因为桌子上已经有了,所以此时厨师只能等着。

因此关于生产者的三步我们就需要改进一下,加个判断即可。

此时厨师已经等着了,一旦它等待,CPU的执行权就会被吃货抢走,吃货就会执行我们刚刚推导的两步,它会去判断桌子上是否有事物,没有事物就会等待。

如果有就开始,吃完后还需要再次唤醒厨师继续做。

说以这才是生产者和消费者完整的执行过程。

image-20240506211114149

在这个过程中涉及到三个方法

方法名说明
void wait()当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法唤醒为止
void notify()随机唤醒单个线程
void notifyAll()唤醒所有线程

二、Desk的代码实现

在刚刚分析的过程中,我们知道在里面至少会有三个角色,分别是 生产者消费者 以及中间那个控制 生产者和消费者的那个第三者

public class Desk {

    /*
    * 作用:控制生产者和消费者的执行
    *
    * */

    //那如何控制呢?在它里面就应该有一个状态,没有食物就是厨师执行,有实物就是吃货执行
    //为了方便外界调用,这里直接使用public static就行了
    //表示桌子上是否有面条  0:没有面条  1:有面条
    //那为什么不使用boolean类型表示呢?因为boolean只有两个值,它只能控制两条线程轮流执行。
    //以后我的需求变更了,需要控制三条线程、四条线程轮流执行,这个boolean就搞不定了。因此这里为了考虑到后面的通用性,这里用int类型
    public static int foodFlag = 0;

    //吃货也不可能一直吃,因此需要有一个变量来表示总个数
    //总个数
    public static int count = 10; // 表示吃货最多可以吃十个

    //在线程中还需要用到锁,因此在这里定一个锁对象
    //锁对象
    public static Object lock = new Object();
}

接下来就是补全吃货和厨师里面的代码就行了。


三、消费者代码实现

大家以后在写多线程的时候,一定要按照之前四个套路去写,有了套路多线程的代码会变得非常简单。

1. 循环
2. 同步代码块(之后可以改写为同步方法或者lock锁,都行)
3. 判断共享数据是否到了末尾(到了末尾),建议先写到了末尾的情况,因为到了末尾更简单
4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
public class Foodie extends Thread {

    @Override
    public void run() {
        //1. 循环
        while (true) {
            //2. 同步代码块(之后可以改写为同步方法或者lock锁,都行)
            //括号中写锁对象,锁对象一定要唯一,在Desk中的静态变量lock,我们将它当做是锁就行了
            synchronized (Desk.lock) {
                //3. 判断共享数据是否到了末尾(到了末尾)
                //什么是共享数据?很简单,你就想线程什么时候才能停止。
                //即当吃货把十碗面条都吃完了,线程就要停止了。因此此时共享数据就是Desk里面的count。它的初始值是10,如果你变成0了,那么循环就要停止。循环一旦停止,循环就会结束了。
                if (Desk.count == 0) {
                    break;
                } else {
                    //4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
                    //先判断桌子上是否有面条
                    if (Desk.foodFlag == 0) {
                        //如果没有,就等待
                        try {
                            //等待的时候不能直接调用wait(),而是需要使用锁对象去调用wait()
                            //为什么是这样呢?其实是有原因的,这行代码会有一个逻辑:让当前线程跟锁进行绑定
                            //一旦绑定后,在下面我们进行notifyAll()唤醒的时候就可以操作了。
                            //因为我们在唤醒的时候肯定不能唤醒操作系统中所有的线程,那我该唤醒哪些线程呢?其实就跟这个锁是有关系的。
                            //我们下面在调用notifyAll()的时候也是通过锁进行调用的,这个时候Dest.lock.notyfyAll()就表示现在要唤醒这把锁绑定的所有线程。
                            //因此我们不管调用wait()还是调用下面notyfyAll(),都是需要用锁对象调用的。
                            //wait()有异常,我们需要对它进行处理,并且它只能try不能抛出
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else { // 这里的else就表示footFlag不是0,即桌子上是有面条的
                        //把吃的总数-1
                        Desk.count--;
                        //如果有,就开吃
                        System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();//表示要去唤醒绑定在这把锁上的所有线程
                        //修改桌子的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}

四、生产者代码实现

首先生产者也是个多线程,多线程中的四个套路千万别忘了。

1. 循环
2. 同步代码块(之后可以改写为同步方法或者lock锁,都行)
3. 判断共享数据是否到了末尾(建议先写到了末尾的情况,因为到了末尾更简单)
4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
public class Cook extends Thread {
    @Override
    public void run() {
        //1. 循环
        while (true) {
            //2. 同步代码块(之后可以改写为同步方法或者lock锁,都行)
            synchronized (Desk.lock) {
                //3. 判断共享数据是否到了末尾
                if (Desk.count == 0) {
                    //到末尾了
                    break;
                } else {
                    //没有到末尾
                    //判断桌子上是否有食物
                    if (Desk.foodFlag == 1) {
                        //如果有,就等待吃货将已有的吃掉
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        //如果没有,就制作食物
                        System.out.println("厨师做了一碗面条");
                        //修改桌子上的食物状态
                        Desk.foodFlag = 1;
                        //叫醒等待的消费者开吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

五、测试

public static void main(String[] args) {
    //创建线程的对象
    Cook c = new Cook();
    Foodie f = new Foodie();

    //给线程设置名字
    c.setName("厨师");
    f.setName("吃货");

    //开启线程
    c.start();
    f.start();
}

程序运行完毕,可以发现就是跟我们想象中是一样的,厨师做一碗,吃货吃一碗…

并且程序也已经停止。如果你写完代码,这里的红灯没有灭,那么你的代码就是有bug的。

image-20240506213927365
  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值