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()方法都是自带锁的,所以我们并不用手动设置锁,同时,由于我们代码中的输出语句在锁的外面

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

相关推荐
Scott9999HH7 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔7 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社8 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海8 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖9 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大12 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai12 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说12 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json