java多线程:线程间通信——生产者消费者模型
一、背景 && 定义
多线程环境下,只要有并发问题,就要保证数据的安全性,一般指的是通过 synchronized 来进行同步。
另一个问题是,多个线程之间如何协作呢?
我们看一个仓库出货问题(更具体一些,快餐店直接放好炸货的架子,不过每次只放一份)
- 假设仓库中只能存放一件商品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走进行消费;
- 如果仓库中没有商品,那么生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
- 如果仓库中放有产品,消费者可快速取走并消费,否则停止消费并等待,直到仓库中再次放入产品为止。
这其实就是一个线程同步问题。生产者和消费者共享同一个资源,并且生产者和消费者之间互相依赖,互为条件。
如果一个快餐店:
先点单,餐出来之后再收钱。这种模式叫BIO-阻塞IO模式。
如果一个快餐店:
先收钱,收完钱消费者在旁边等。这种就是生产者-消费者模式。
这类问题里,同步的候只有 synchronized 是不够的,因为他虽然能解决资源的共享问题,实现资源的同步更新,但是无法在不同线程之间进行消息传递(通信)。
所以只有我们之前所说的加锁和排队是不够的,还要有通知。
定义:
生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
为了解决双方能力不等而等待的问题,引入对应的解决方案。生产者消费者模型是一种并发协作模型。
二、解决方式介绍
2.1 管程法
- 生产者:负责生产数据的模块(模块可能是方法、对象、线程、进程);
- 消费者:负责处理数据的模块(模块可能是方法、对象、线程、进程);
- 缓冲区:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”(缓冲区一般是队列)。
生产者和消费者都是通过缓冲区进行数据的 放 和 拿 。
这样的话,一来可以避免旱的旱死,涝的涝死的问题:不管哪一方过快或者过慢,缓冲区始终有一部分数据;二来能够达到生产者和消费者的解耦,不再直接通信,从而提高效率。
因为容器相当于一个输送商品的管道,所以成为管程法。
2.2 信号灯法
采用类似红灯绿灯的模式,决定车走还是人走。
- 管程法使用容器的状态来控制,数据在容器中;
- 而信号灯法只是用信号来给生产者和消费者提醒,他们的交互数据并不由信号灯来保管。
2.3 Object类
jdk 里面 Object 类老早就有提供解决线程间通信的问题的方法:
- wait():表示线程一直等待,直到其他线程通知(也就是调用了notify或者notifyAll方法),与sleep不同,会释放锁;
- wait(long timeout):指定时间;
- notify():唤醒一个处于等待状态的线程;
- notifyAll():唤醒同一个对象上所有调用 wait() 方法的线程,优先级别高的线程优先调度。
这几个方法都是在同步方法或者同步代码块中使用,否则会抛出异常。
(很多面试题问 Java 的 Object 类有哪些方法,都是希望得到关于这块的答案,引到多线程)
三、管程法实现
管程法实现的四个角色:
- 生产者和消费者都是多线程;
- 中间的缓冲区应该是一个容器,并且需要的是一个并发容器,java.util.concurrent包里面已经提供了;
- 资源,也就是各个角色来回交换的商品。
利用 Object 类的几个方法,来实现管程法,以下是代码示例:
/**
* 协作模型:生产者消费者模型实现:管程法
*/
public class Cooperation1 {
public static void main(String[] args) {
Container container = new Container();
new Producer(container).start();
new Consumer(container).start();
}
}
/**
* 生产者
*/
class Producer extends Thread{
Container container;
public Producer(Container container){
this.container = container;
}
@Override
public void run() {
//生产过程
for (int i=0; i<10; i++){
System.out.println("生产第 " + i + " 个馒头");
container.push(new Hamburger(i));
}
}
}
/**
* 消费者
*/
class Consumer extends Thread{
Container container;
public Consumer(Container container){
this.container = container;
}
@Override
public void run() {
//消费过程
for (int i=0; i<10; i++){
System.out.println("消费第 " + container.pop().id + " 个馒头");
}
}
}
/**
* 缓冲区,操作商品,并和生产者、消费者交互
*/
class Container{
Hamburger[] food = new Hamburger[10];
private int count = 0;
//存储:生产
public synchronized void push(Hamburger hamburger){
if (count == food.length){
try {
this.wait();//阻塞,但是等待消费者通知后会解除
} catch (InterruptedException e) {
e.printStackTrace();
}
}
food[count++] = hamburger;
this.notifyAll();//说明存在数据了,通知消费者消费
}
//获取:消费
public synchronized Hamburger pop(){
if (count ==0 ){
try {
this.wait();//阻塞,直到生产者通知后会解除
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Hamburger ans = food[--count];
this.notifyAll();//存在空余空间了,通知生产者生产
return ans;
}
}
/**
* 商品
*/
class Hamburger{
int id;
public Hamburger(int id) {
this.id = id;
}
}
其中的核心有这么几点:
- 容器相当于一个栈,是后进先出的;
- 容器的两个方法对于资源的操作,一个和生产者交互,一个和消费者交互,除了 synchronized 修饰,因为两个方法是互斥的,所以利用 wait 和 notify 方法使他们完成阻塞和解除阻塞;
- 生产者和容器交互,添加数据;
- 消费者和容器交互,删除数据。
前面关于 线程的阻塞问题,生命周期里的阻塞,完整的可能情况,就包含这里的阻塞情况:

四、信号灯法实现
和上一种通过容器的容量让线程之间互相通知的方法不同,信号灯法没有用数据缓存的方式,而是用信号灯来指示双方,对方是否已经准备好了要和你通信。
下面是一个 电视直播和观众的代码示例,通过信号灯,通知演员和观众直播,确保演员在演的时候,让观众来看。
/**
* 协作模型:生产者消费者实现:信号灯法
*/
public class Cooperation2 {
public static void main(String[] args) {
TV tv = new TV();
new Actor(tv).start();
new Fans(tv).start();
}
}
/**
* 生产者:演员
*/
class Actor extends Thread{
TV tv;
public Actor(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i=0; i<10; i++){
if (i%2 == 0){
this.tv.play("节目 " + i);
}else{
this.tv.play("广告 " + i);
}
}
}
}
/**
* 消费者:观众
*/
class Fans extends Thread{
TV tv;
public Fans(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i=0; i<10; i++){
tv.watch();
}
}
}
/**
* 共同资源:电视直播
*/
class TV{
String voice;
//信号灯,如果为真则演员准备,观众等待
//如果为假,则观众就位,演员等待
boolean flag = true;
//表演方法:针对生产者
public synchronized void play(String voice){
//演员等待
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.voice = voice;
System.out.println("表演 "+voice +" ing");
//唤醒观众
this.notifyAll();
this.flag = !flag;
}
//观看方法:针对消费者
public synchronized void watch(){
//观众等待
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看 " + voice +" ing");
this.notifyAll();
this.flag = !flag;
}
}
可以看到,相比管程法的核心区别是:
TV 没有用一个容器存储数据,只是通过生产者是否生产,来决定信号灯的标志,以此通知消费者来消费。
显然这两种实现方法,有不同的适用场景,那就是决定于生产者消费者是否有数据沟通。
java多线程:线程间通信——生产者消费者模型的更多相关文章
- Java多线程——线程间通信
Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...
- Java线程间通信--生产者消费者
class ProducerConsumerDemo { public static void main(String[] args) { Resource r = new ...
- 线程间通信--生产者消费者 升级版JDK5
import java.util.concurrent.locks.*; /*1.新的解锁,上锁操作,据说是jdk5.0升级版,以前的枷锁,解锁都是隐藏的,默认的,现在变成显式 2.新的异常处理方式 ...
- 第23章 java线程通信——生产者/消费者模型案例
第23章 java线程通信--生产者/消费者模型案例 1.案例: package com.rocco; /** * 生产者消费者问题,涉及到几个类 * 第一,这个问题本身就是一个类,即主类 * 第二, ...
- 【Java并发编程】:生产者—消费者模型
生产者消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据. 这里实现如下情况的生产--消费模型: 生产者不断交替地生产两组数据“姓 ...
- 线程锁、threading.local(flask源码中用的到)、线程池、生产者消费者模型
一.线程锁 线程安全,多线程操作时,内部会让所有线程排队处理.如:list/dict/Queue 线程不安全 + 人(锁) => 排队处理 1.RLock/Lock:一次放一个 a.创建10个线 ...
- 锁丶threading.local丶线程池丶生产者消费者模型
一丶锁 线程安全: 线程安全能够保证多个线程同时执行时程序依旧运行正确, 而且要保证对于共享的数据,可以由多个线程存取,但是同一时刻只能有一个线程进行存取. import threading v = ...
- 守护进程,进程安全,IPC进程间通讯,生产者消费者模型
1.守护进程(了解)2.进程安全(*****) 互斥锁 抢票案例3.IPC进程间通讯 manager queue(*****)4.生产者消费者模型 守护进程 指的也是一个进程,可以守护着另一个进程 一 ...
- Java并发——线程间通信与同步技术
传统的线程间通信与同步技术为Object上的wait().notify().notifyAll()等方法,Java在显示锁上增加了Condition对象,该对象也可以实现线程间通信与同步.本文会介绍有 ...
随机推荐
- 一步步教你用Prometheus搭建实时监控系统系列(一)——上帝之火,普罗米修斯的崛起
上帝之火 本系列讲述的是开源实时监控告警解决方案Prometheus,这个单词很牛逼.每次我都能联想到带来上帝之火的希腊之神,普罗米修斯.而这个开源的logo也是火,个人挺喜欢这个logo的设计. 本 ...
- RN开发杂记
获取屏幕尺寸const window = Dimensions.get('window');const screenHeight = Platform.OS === 'ios' ? window.he ...
- control+B进入layout文件的xml文本编辑模式
control+B进入layout文件的xml文本编辑模式
- 什么是 A/B 测试?
1.什么是A/B 测试?有什么用? 做过App功能设计的读者朋友可能经常会面临多个设计方案的选择,例如某个按钮是用蓝色还是黄色,是放左边还是放右边. 传统的解决方法通常是集体讨论表决,或者由某位专家或 ...
- MacOS IDEA下SVN配置与使用
第一部分,准备工作 到IDEA的配置下设置SVN命令,一般来说,IDEA已经设置好了,如果没有自己找到存放SVN命令的目录,将SVN配置进去,命令应该存放在/Library/Developer/Com ...
- PHP array_keys() 函数
实例 返回包含数组中所有键名的一个新数组: <?php$a=array("Volvo"=>"XC90","BMW"=>&q ...
- PHP curl_share_close函数
(PHP 5 >= 5.5.0) curl_share_close — 关闭 cURL 共享句柄 说明 void curl_share_close ( resource $sh ) 关闭 cUR ...
- luogu P3412 仓鼠找sugar II 期望 树形dp
LINK:仓鼠找sugar II 以前做过类似的期望题目 加上最后的树形dp不算太难 还是可以推出来的. 容易发现 当固定起点和终点的时候 可以先固定根 这样就不用分到底是正着走还是倒着走了. 1为根 ...
- 最详尽的datagrip使用
什么是datagrip? datagrip是jetbrains旗下的一款数据库管理工具,相信做过java开发的都知道,idea就是这家公司发明的. 为什么要使用datagrip? datagrip的有 ...
- 同事问我MySQL怎么递归查询,我懵逼了
前言 最近在做的业务场景涉及到了数据库的递归查询.我们公司用的 Oracle ,众所周知,Oracle 自带有递归查询的功能,所以实现起来特别简单. 但是,我记得 MySQL 是没有递归查询功能的,那 ...