Java_多线程_生产者消费者模型_互斥锁,阻塞队列

生产者消费者模型(Producer-Consumer Model)是计算机科学中一个经典的并发编程模型,用于解决多线程/多进程环境下的协作问题。

java 复制代码
基本概念
生产者:负责生成数据或任务的实体
消费者:负责处理数据或执行任务的实体
缓冲区:生产者与消费者之间共享的数据存储区域
java 复制代码
模型特点
生产者与消费者以不同的速度运行
两者通过共享的缓冲区进行通信
缓冲区有大小限制,可能满或空
java 复制代码
需要解决的问题
同步问题:
当缓冲区满时,生产者需要等待
当缓冲区空时,消费者需要等待
互斥问题:
对缓冲区的访问必须是互斥的,防止数据竞争
java 复制代码
常见实现方式
使用信号量(Semaphore):
	空缓冲区信号量
	满缓冲区信号量
	互斥信号量
使用条件变量(Condition Variable)和互斥锁(Mutex)
使用阻塞队列(高级语言中常用)

本篇我们使用互斥锁和阻塞队列来解决这个问题

在多线程的锁中我们首先要避免的就是死锁这个问题,在 Java 中,Lock 接口及其实现类(如 ReentrantLock)是在 JDK 5(Java 5) 引入的,属于java.util.concurrent.locks 包的一部分。我们这里使用Java的synchronized实现

首先来分析问题,我们可以抽象的将生产者消费者问题想象为,厨师和顾客的问题:

顾客:

  1. 判断桌子上是否有食物
  2. 如果没有就等待
  3. 如果有就直接吃掉
  4. 吃完食物之后,通知厨师继续做食物

厨师:

  1. 判断桌子上是否有食物
  2. 如果桌子上有食物的话就等待
  3. 如果桌子上没有食物的话就制作食物
  4. 将食物放置在桌子上
  5. 唤醒等待的顾客开始吃

分析完毕,首先我们应该先新建三个类分别为:Cook(厨师类),Customer(顾客类),Desk(桌子类)

初始化桌子:

  1. 初始化桌子上食物的标志,0为无食物,1为有食物
  2. 初始化顾客的上限,例如顾客最多吃10份食物
java 复制代码
package Thread.Producer_Consumer;

public class Desk {
    public static int FoodFlag=0;//食物当前的状态表示当前桌子上是否有食物
    public static int count=10;//消费者最多可以吃10个食物
    public static Object lock=new Object();////创建一个锁对象,用于生产者和消费者线程间的同步
}

//由于是所有线程共同的变量所以我们使用static关键字修饰

接下来开始完成顾客线程

java 复制代码
package Thread.Producer_Consumer;

public class Customer extends  Thread{
    //消费者线程
    @Override
    public void run() {
        while( true){
            synchronized(Desk.lock){
                // 检查是否达到食物上限(count=0表示不能再吃)
                if(Desk.count==0){
                    break;
                }
                else {
                    // 检查桌子上是否有食物(FoodFlag=1表示有食物)
                    if (Desk.FoodFlag == 1) {
                        System.out.println("顾客吃掉食物");
                        Desk.FoodFlag = 0;//表示没有食物
                        Desk.count--;//剩余可吃食物数量减1
                        System.out.println("顾客还可以吃"+Desk.count);
                        Desk.lock.notifyAll();//唤醒lock锁中所有等待的线程
                    } else {
                    //桌上没有食物,则等待
                        try {
                            Desk.lock.wait();//释放锁,并进入等待状态
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        }
        System.out.println("顾客吃饱了,结束消费!");
    }
}

厨师线程

java 复制代码
package Thread.Producer_Consumer;

public class Cook extends Thread{


    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock){
                //如果消费者可以吃的食物的数量已经达到最大,那么则直接退出
                if (Desk.count==0) {
                    break;
                }
                else{
                    //如果桌子有食物,等待消费者进程
                    if(Desk.FoodFlag==1){
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }

                    }else{
                        //如果桌子没有食物
                        System.out.println("生产者正在生产食物...");
                        //设置桌子有食物
                        Desk.FoodFlag=1;
                        //唤醒消费者线程
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

最后创建一个测试类

java 复制代码
package Thread.Producer_Consumer;

public class Test {
    public static void main(String[] args) {
        Desk desk=new Desk();
        Customer f1=new Customer();
        Cook c1=new Cook();

        f1.setName("消费者1");
        c1.setName("生产者1");

        f1.start();
        c1.start();
    }
}

下来我们使用阻塞队列来实现一下:

桌子类:

java 复制代码
package Thread.Producer_Consumer_2;

public class Desk {
    public static int count=10;//消费者最多可以吃10个食物
    public static int Food_max=10;
}

厨师类:

java 复制代码
package Thread.Producer_Consumer_2;

import java.util.concurrent.ArrayBlockingQueue;

public class Cook extends  Thread{
    ArrayBlockingQueue<String> queue;
    public Cook(ArrayBlockingQueue<String> queue){this.queue=queue;}//构造方法,创建一个阻塞队列

    @Override
    public void run() {
        //厨师不断将食物放进队列中
        while(true){
            if(Desk.Food_max<=0){
                break;
            }else{
                try {
                    queue.put("食物");
                    System.out.println("厨师放了一个食物");
                    Desk.Food_max--;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        }
    }
}

顾客类:

java 复制代码
package Thread.Producer_Consumer_2;

import Thread.Producer_Consumer.Desk;

import java.util.concurrent.ArrayBlockingQueue;

public class Customer extends  Thread{
    //消费者线程
    ArrayBlockingQueue<String> queue;
    public Customer(ArrayBlockingQueue<String> queue)
    {
        this.queue=queue;
    }

    @Override
    public void run() {
        while( true){
            if(Desk.count==0){
                System.out.println("顾客吃到上限了");
                break;
            }else{
                try {
                    queue.take();
                    System.out.println("消费者吃掉了一个食物");
                    Desk.count--;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        }
    }
}

测试类:

java 复制代码
package Thread.Producer_Consumer_2;

import Thread.Producer_Consumer.Desk;

import java.util.concurrent.ArrayBlockingQueue;

public class Test {
    public static void main(String[] args) {
        ArrayBlockingQueue<String> list=new ArrayBlockingQueue<>(5);//创建一个阻塞队列

        Customer f1=new Customer(list);
        Cook c1=new Cook(list);

        f1.setName("消费者1");
        c1.setName("生产者1");

        f1.start();
        c1.start();
    }
}

阻塞队列总结:

在创建阻塞队列的时候,需要创建实现类的对象,我们可以通过查看源码的形式来看一下

ArrayBlockingQueue实现了BlockingQueue这个接口

BlockingQueue又继承于Queue

Queue又继承于Collection

Collection又继承于Iterable,由此我们也就可以得出,阻塞队列是可以通过for_each循环遍历的

回归主题,在ArrayBlockingQueue中,有一个带参的构造方法,由此来创建阻塞队列,capacity代表阻塞队列的容量(不要忘记了泛型代表了阻塞队列的参数为哪种类型)

同时,LinkBlockingQueue也实现了BlockingQueue这个接口

由此可以得出一个图:


由于put()和take()方法都是自带锁的,所以我们并不用手动设置锁,同时,由于我们代码中的输出语句在锁的外面

这导致了输出时偶尔并不能按照我们的想法进行输出,但是执行时一定是正确的

相关推荐
执笔诉情殇〆几秒前
SpringBoot3(若依框架)集成Mybatis-Plus和单元测试功能,以及问题解决
java·spring boot·junit·mybatis-plus
汤姆大聪明5 分钟前
SSM框架中关于Spring MVC的技术问题
java·spring·mvc
景天科技苑14 分钟前
【Rust线程池】如何构建Rust线程池、Rayon线程池用法详细解析
开发语言·后端·rust·线程池·rayon·rust线程池·rayon线程池
~央千澈~18 分钟前
Go、Node.js、Python、PHP、Java五种语言的直播推流RTMP协议技术实施方案和思路-优雅草卓伊凡
java·python·go·node
液态不合群23 分钟前
JavaScript 编年史:探索前端界巨变的幕后推手
开发语言·前端·javascript
yzx9910131 小时前
JS与Go:编程语言双星的碰撞与共生
java·数据结构·游戏·小程序·ffmpeg
荼蘼1 小时前
用Python玩转数据:Pandas库实战指南(二)
开发语言·python·pandas
油丶酸萝卜别吃1 小时前
JS深度克隆对象(克隆时包含函数)
开发语言·javascript·ecmascript
牛客企业服务1 小时前
AI面试与传统面试的核心差异解析——AI面试如何提升秋招效率?
java·大数据·人工智能·python·面试·职场和发展·金融
懒虫虫~1 小时前
Metaspace耗尽导致OOM问题
java