Java生产消费者模型——代码解析
我们将生产者、消费者、库存、和调用线程的主函数分别写进四个类中,通过抢夺非线程安全的数据集合来直观的表达在进行生产消费者模型的过程中可能出现的问题与解决办法。
我们假设有一个生产者,两个消费者来共同抢夺库存里的资源,而生产者和消费者都以线程来实现。
库存对象只有是唯一的才会出现抢夺一个资源的可能,所以为了使库存对象是唯一的,我们可以使用两种方法实现,单例模式和通过生产者和消费者的构造函数参数来初始化。
本次举例使用的是构造函数的方法,但代码中也注释出了单例模式的写法与使用。
先创建一个简单的生产消费者模型,查看它的运行结果。
库存类:
package producterac;
import java.util.ArrayList;
public class WareHouse {
//存放非线程安全的数组的集合
private ArrayList<String> list = new ArrayList<String>();
/*
* //创建单例模式使生产消费者操作的是同一库存对象
* private WareHouse() {}
* //建立静态对象以在初始化的时候建立仅一个库存对象
* private static WareHouse wh = new WareHouse();
*
* //将方法设置为静态是因为在无法new库存对象的情况下,
* //我们可以通过将方法设定为静态来直接通过类名调用静态方法
* public static WareHouse getInstance() {
* return wh;
* }
*/
//写生产者操作仓库的方法
public void add() {
if(list.size() < 20) {
list.add("一个数据");
}else {
//数据存够之后直接返回,不运行存储数据的操作
return;
}
}
//写消费者操作仓库的操作
public void get() {
//判断集合中是否还有数据可以取出
//如果不判断会造成集合越界
if(list.size() > 0) {
list.remove(0);
}else {
return;
}
}
}
- 生产者类:
package producterac;
public class Producter extends Thread{
private String pName;
//我们要使生产者和消费者操控同一个库存对象
//也可以使用单例模式来建立库存对象
private WareHouse wh;
public Producter(String pName,WareHouse wh) {
this.pName = pName;
this.wh = wh;
}
//重写run方法
public void run() {
while(true) {
wh.add();
System.out.println("生产者"+pName+"添加了一个货物");
try {
//使线程等待一会儿
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
- 消费者类:
package producterac;
public class Consumer extends Thread{
private String cName;
//获取库存对象
/* private WareHouse wh = WareHouse.getInstance(); */
//我们要使生产者和消费者操控同一个库存对象
//也可以使用单例模式来建立库存对象
private WareHouse wh;
public Consumer(String cName,WareHouse wh) {
this.cName = cName;
this.wh = wh;
}
//重写run方法
public void run() {
while(true) {
wh.get();
System.out.println("消费者"+cName+"拿走了一个货物");
try {
//使线程等待一会儿
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
- 主函数类:
package producterac;
public class Main {
public static void main(String[] args) {
WareHouse wh = new WareHouse();
Producter p1 = new Producter("1", wh);
Consumer c1 = new Consumer("1", wh);
Consumer c2 = new Consumer("2", wh);
p1.start();
c1.start();
c2.start();
}
}
部分运行结果:

我们看到出现 java.lang.ArrayIndexOutOfBoundsException异常,说明消费者在拿走货物的时候集合越界没有拿到,所以出现了异常。
即使我们在库存的get()方法中判断了集合是否为空,但也还是出现了异常。原因是因为在两个线程同时访问一个对象的时候,有可能当线程1刚判断完集合不为空进入了if循环但还没有拿走货物的情况下,线程2也进行了get()方法先线程1一步拿走了最后的一个货物,然后当线程1想拿走货物的时候集合里已经没有了,这种情况下就会发生上述异常。
这就造成了线程抢夺资源时非安全的问题,那么我们可以将库存对象使用线程锁synchronized锁起来,这样在一个消费者访问库存对象的时候其他消费者无法访问库存对象,从而解决集合越界问题,使线程安全。
- 修改过的库存类(加入了synchronized修饰符的add()和get()方法):
//写生产者操作仓库的方法
public synchronized void add() {
if(list.size() < 20) {
list.add("一个数据");
}else {
//数据存够之后直接返回,不运行存储数据的操作
return;
}
} //写消费者操作仓库的操作
public synchronized void get() {
//判断集合中是否还有数据可以取出
//如果不判断会造成集合越界
if(list.size() > 0) {
list.remove(0);
}else {
return;
}
}
使用synchronized修饰符修饰库存方法之后就不会报错了!
我们也可以将return替换为wait()方法让线程等待,将编写的生产消费者模型中的return修改为wait()。
- 修改过的库存类:
//写生产者操作仓库的方法
public synchronized void add() {
if(list.size() < 20) {
list.add("一个数据");
}else {
try {
//这个this指的是访问库存对象的线程wait,不是库存对象wait
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} //写消费者操作仓库的操作
public synchronized void get() {
//判断集合中是否还有数据可以取出
//如果不判断会造成集合越界
if(list.size() > 0) {
list.remove(0);
}else {
try {
//这个this指的是访问库存对象的线程wait,不是库存对象wait
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果:

我们会发现到最后所有的线程都会处于wait等待状态,运行到最后没有线程在执行了。所以我们需要在其中一个线程等待的时候将其他线程继续唤醒,保持系统的运行。
唤醒线程可以使用notify/notifyAll()方法。
- 再次修改后的库存类:
//写生产者操作仓库的方法
public synchronized void add() {
if(list.size() < 20) {
list.add("一个数据");
}else {
try {
//因为我们无法知道哪个线程是消费者线程,所以我们要将线程全部唤醒
this.notifyAll();
//这个this指的是访问库存对象的线程wait,不是库存对象wait
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} //写消费者操作仓库的操作
public synchronized void get() {
//判断集合中是否还有数据可以取出
//如果不判断会造成集合越界
if(list.size() > 0) {
list.remove(0);
}else {
try {
//因为我们无法知道哪个线程是生产者线程,所以我们要将线程全部唤醒
this.notifyAll();
//这个this指的是访问库存对象的线程wait,不是库存对象wait
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行成功!说明我们这时候真正地实现了简单的生产消费者模型。
附:如果将完成的生产消费者模型中add()和get()方法的synchronized修饰符去掉,会发生如下错误。

将synchronized修饰符去掉后,发生了java.lang.IllegalMonitorStateException异常,原因是当线程1进入else要执行wait()方法的那个时刻,线程2也进入了库存对象中,致使当wait()方法真正执行的时候wait的是线程2而不是线程1,发生这种情况的时候就会发生上述异常。
Java生产消费者模型——代码解析的更多相关文章
- JAVA实现生产消费者模型
前言 最近面试比较多,发现生产消费者模型在各公司面试的过程中问的还是比较多的,记录一下常见JAVA实现生产者消费模型的代码 思路 我们通过三种模式来实现 通过wait和notify 通过Lock和Co ...
- Linux——多线程下解决生产消费者模型
我们学习了操作系统,想必对生产消费者问题都不陌生.作为同步互斥问题的一个经典案例,生产消费者模型其实是解决实际问题的基础模型,解决很多的实际问题都会依赖于它.而此模型要解决最大的问题便是同步与互斥.而 ...
- Python进阶----进程之间通信(互斥锁,队列(参数:timeout和block),), ***生产消费者模型
Python进阶----进程之间通信(互斥锁,队列(参数:timeout和block),), ***生产消费者模型 一丶互斥锁 含义: 每个对象都对应于一个可称为" 互斥锁&qu ...
- Python之queue模块以及生产消费者模型
队列 队列类似于一条管道,元素先进先出,进put(arg),取get() 有一点需要注意的是:队列都是在内存中操作,进程退出,队列清空,另外,队列也是一个阻塞的形态. 队列分类 队列有很多中,但都依赖 ...
- Python并发编程04 /多线程、生产消费者模型、线程进程对比、线程的方法、线程join、守护线程、线程互斥锁
Python并发编程04 /多线程.生产消费者模型.线程进程对比.线程的方法.线程join.守护线程.线程互斥锁 目录 Python并发编程04 /多线程.生产消费者模型.线程进程对比.线程的方法.线 ...
- Python - Asyncio模块实现的生产消费者模型
[原创]转载请注明作者Johnthegreat和本文链接 在设计模式中,生产消费者模型占有非常重要的地位,这个模型在现实世界中也有很多有意思的对应场景,比如做包子的人和吃包子的人,当两者速度不匹配时, ...
- Java生产者消费者模型
在Java中线程同步的经典案例,不同线程对同一个对象同时进行多线程操作,为了保持线程安全,数据结果要是我们期望的结果. 生产者-消费者模型可以很好的解释这个现象:对于公共数据data,初始值为0,多个 ...
- java生产者消费者问题代码分析
作者要的是一个生产者生成,接着必须有一个消费者消费,那这不是需要单线程吗?或者使用1个大小的阻塞队列.所以只谈论问题本身,不谈论好不好. 具体代码: import java.util.concurre ...
- Python——Queue模块以及生产消费者模型
1.了解Queue Queue是python标准库中的线程安全的队列(FIFO)实现,提供了一个适用于多线程编程的先进先出的数据结构,即队列,用来在生产者和消费者线程之间的信息传递 |queue.Qu ...
随机推荐
- Edraw Max 9.4 Crack Method
使用010editor修改以下两个文件. BaseCore.dll (修改二进制内容hex) Before C6 45 C8 62 C6 45 C9 64 C6 45 CA 65 C6 45 CB 6 ...
- nuxtjs在vue组件中使用window对象编译报错的解决方法
我们知道nuxtjs是做服务端渲染的,他有很多声明周期是运行在服务端的,以及正常的vue声明周期mounted之前均是在服务端运行的,那么服务端是没有比如window对象的location.navag ...
- shell 空语句
在shell脚本中“:”是空命令,表示什么都不做类似于python中的pass
- rancher2基础环境配置
一.主机配置 1.配置要求 参考节点要求 2.主机名配置 因为K8S的规定,主机名只支持包含 - 和 .(中横线和点)两种特殊符号,并且主机名不能出现重复. 3.Hosts 配置每台主机的hosts( ...
- sql脱库的几种方法
当发现sql注入之后,脱库的方法,有以下几种: (1)当目标主机支持外部连接时,使用Navicat 进行连接!当时目标主机不同,使用的Navicat种类不一样: mysql : Navicat f ...
- 【转】linux sed命令
转自:linux sed命令就是这么简单 参考:Linux三大剑客之sed:https://blog.csdn.net/solaraceboy/article/details/79272344 阅读目 ...
- 屏幕方向读取与锁定:Screen Orientation API(转)
什么是 Screen Orientation API Screen Orientation API 为 Web 应用提供了读取设备当前屏幕方向.旋转角度.锁定旋转方向.获取方向改变事件的能力.使得特定 ...
- Exit 与 Goto :eof 在批处理中的区别【转】
在 CMD 命令提示符窗口直接运行: 1.) 运行 Goto :eof 后,CMD 返回并将等待下一命令. 2.) 运行 Exit 后,CMD 将直接关闭并返回到曾启动 Cmd.exe 的程序或返回到 ...
- 工具系列 | PHPSTROM 连接Docker容器 && 配置XDEBUG调试
Docker 客户端配置 PHPSTROM 配置 选择连接 容器日志 配置Xdebug 开启Debug模式 打断点 浏览器访问该项目地址:http://wiot.frp.tinywan.top/
- C++生成随机数(随机整数/浮点数)方法
来源:https://blog.csdn.net/u014571489/article/details/82258467 产生一定范围随机数的通用表示公式要取得[a,b)的随机整数,使用(rand() ...