一、思路分析
1)引入
生产者和消费者
也叫做 等待唤醒机制
。
生产者消费者模式
是一个十分经典的多线程协作的模式,学习完了 等待唤醒机制
,会让我们对多线程的运行有更加深刻的理解。
之前我们学习过,线程的执行是有随机性的,所以如果有两条线程正在执行,它的结果有可能是这样的,完全随机。
但是我们现在学习的 等待唤醒机制
,这个机制就要打破随机的规则。
它会让两个线程轮流执行,你一次我一次。这就是在 等待唤醒机制
中多线程的运行结果。
其中的一条线程我们会将它叫做 生产者
,负责 生产数据
。
另一条线程我们会叫做 消费者
,负责 消费数据
。
由于这个过程非常的复杂,我们先将一个小故事,用故事去理解复杂的代码逻辑。
假设现在有两个人,一个是吃货,一个是厨师。
吃货负责吃,所以它是消费者;厨师负责做,所以它是生产者。
但是光有它们两个还不够,还需要有第三者桌子,因为在默认情况下,线程的执行是具有随机性的,我们需要有一个东西去控制线程的执行,现在我们就用桌子去控制。
![image-20240506204847961](https://img-blog.csdnimg.cn/img_convert/341717b04439f3ad250b847a1ebd33cc.png)
假设现在桌子上有一碗面条,如果有面条,就是吃货执行,负责吃。
![image-20240506204951698](https://img-blog.csdnimg.cn/img_convert/8da7b499a252087b6785cfeeb1767beb.png)
但是如果桌子上没有面条,那就是厨师去执行,它负责做面条。
![image-20240506205024343](https://img-blog.csdnimg.cn/img_convert/fbb92edc114afbcd3f9b8e164c341477.png)
我们先来看看理想情况是怎么样的。
理想情况下,就是一开始是厨师先抢到了CPU的执行权,此时桌子上是没有面条的,所以厨师在一开始就要做一碗面条。
做完了后将买那条放在桌子上,再让吃货线程来吃,这就是理想的情况。
相当于就是厨师做一个,吃货吃一个。
但是程序程序没有这么听话,它不能按照我们自己想的去执行,线程的执行是具有随机性的。
所以我们需要将各种各样的情况给考虑到。
但是它出现的情况没有大家想的那么复杂,只有两种情况。
我们先来分析第一种:消费者等待。
2)情况一:消费者等待
假设在一开始,不是厨师先抢到CPU的执行权,而是吃货先抢到CPU的执行权,这个时候桌子上还没有东西的,总不能啃桌子吧,所以它只能先等着,代码中叫做 wait
。
一旦它等了,CPU的执行权一定会被厨师抢到,厨师就会看下桌子上有没有面条,现在没有,那就开始做,做一碗面条放在桌子上。
做完后还没完,吃货还在等着,因此厨师需要含吃货去吃,这个动作我们也叫作 唤醒
,英文:notify
。
![image-20240506210208638](https://img-blog.csdnimg.cn/img_convert/922debcfd9d7b87b1d980d66043c7dcb.png)
吃货一旦被唤醒后就开始。
![image-20240506210245533](https://img-blog.csdnimg.cn/img_convert/2cb66c01eeae100768d18474788c4aa0.png)
以上就是第一种消费者等待的情况。
主要核心思想其实就是看桌子,桌子上如果没有面条,消费者就会等待。
消费者和生产者执行过程如下
![image-20240506210403572](https://img-blog.csdnimg.cn/img_convert/4758a227dcbb713e5a16d62e04686bfd.png)
3)情况二:生产者等待
在一开始的时候是厨师抢到了CPU的执行权,此时桌子上是没有事物的,此时厨师就会执行我们刚刚分析到的三步:制作食物、把事物放在桌子上、叫醒等待的消费者开吃。
但是现在没有人在等,这个其实也没有什么太大关系,就是喊一下的事情,代码是不会受到任何影响的。
![image-20240506210832037](https://img-blog.csdnimg.cn/img_convert/57b014c0ce0d82d8c7eb5b67b2108f7e.png)
这个时候,就不能是左边的吃货去抢到CPU的执行权了,如果是吃货抢到了CPU的执行权,此时就变成理想情况了。
而我们现在说的是生产者等待的情况,因此下一次还是厨师抢到CPU的执行权。
此时厨师抢到CPU执行权后,她就不能做面条了,因为桌子上已经有了,所以此时厨师只能等着。
因此关于生产者的三步我们就需要改进一下,加个判断即可。
此时厨师已经等着了,一旦它等待,CPU的执行权就会被吃货抢走,吃货就会执行我们刚刚推导的两步,它会去判断桌子上是否有事物,没有事物就会等待。
如果有就开始,吃完后还需要再次唤醒厨师继续做。
说以这才是生产者和消费者完整的执行过程。
![image-20240506211114149](https://img-blog.csdnimg.cn/img_convert/89a4b16131d47139989cb56c19a61425.png)
在这个过程中涉及到三个方法
方法名 | 说明 |
---|---|
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](https://img-blog.csdnimg.cn/img_convert/a3924bfeee862ba40d4c132a31cb6603.png)