多线程JUC:等待唤醒机制(生产者消费者模式)

news/2024/5/20 6:51:03 标签: java, 开发语言, JUC, javase, 面试

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:多线程&JUC:解决线程安全问题——synchronized同步代码块、Lock锁
📚订阅专栏:多线程&JUC
希望文章对你们有所帮助

等待唤醒机制(生产者消费者模式)

  • 等待唤醒机制
  • 等待唤醒机制的实现
    • 消费者代码实现
    • 生产者代码实现
  • 阻塞队列实现等待唤醒机制

等待唤醒机制

等待唤醒机制也叫做生产者消费者模式,打破了以前线程间执行的随机性,生产者消费者模式能够使得线程之间是轮流运行的。是一个非常经典的多线程协作的模式。
对于两条线程,其中一条为生产者,另一条为消费者,大家都是学习过操作系统的,原理多少还是记得一些的。

对于等待唤醒机制,其只有2种情况:

1、消费者等待:若没有可以被消费者消费的数据,那么消费者就是进入wait状态,这时候生产者就可以抢占CPU生产数据,接着notify(唤醒)消费者
2、生产者等待:若已经有数据供给消费者消费,则生产者进入wait状态,消费者抢占CPU消费数据,接着notify(唤醒)生产者

在这其中可能会涉及到的方法:

方法名称说明
void wait()当前线程等待,直到被其他线程唤醒
void notify()随机唤醒单个线程
void notifyAll()唤醒所有线程

等待唤醒机制的实现

消费者代码实现

消费者和生产者中间有一个控制他们执行相应操作的核心,视为Controller,记录一些状态变量和锁对象:

java">public class Controller {
    /**
     * 控制消费者和生产者的执行
     */
    //表示是否有数据 0:没有 1:有
    public static int flag = 0;

    //消费者最多可以消费的数据量
    public static int count = 10;

    //锁对象
    public static Object lock = new Object();
}

接着实现消费者的逻辑:

java">public class Consumer extends Thread{
    @Override
    public void run() {
        while(true){
            synchronized (Controller.lock) {
                if(Controller.count == 0){
                    //消费者已经消费量了10次,退出
                    break;
                }else{
                    //先判断有无可以消费的数据
                    if(Controller.flag == 0) {
                        //若无,等待
                        //用lock调用wait方法,使得当前线程与锁进行绑定,之后唤醒就唤醒这些被绑定了的线程
                        try {
                            Controller.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else{
                        //若有,消费
                        System.out.println("正在消费,还可以消费" + --Controller.count + "个");
                        //消费完后唤醒生产者,唤醒绑定在这把锁上的所有线程
                        Controller.lock.notifyAll();
                        //修改控制中心的状态
                        Controller.flag = 0;
                    }
                }
            }
        }
    }
}

生产者代码实现

java">public class Producer extends Thread{
    @Override
    public void run() {
        while (true){
            synchronized (Controller.lock){
                if(Controller.count == 0){
                    break;
                }else{
                    if(Controller.flag == 1){
                        //已经有供给消费者进行消费的数据
                        try {
                            Controller.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else{
                        System.out.println("成功生产");
                        Controller.lock.notifyAll();
                        Controller.flag = 1;
                    }
                }
            }
        }
    }
}

最后编写测试类代码验证:

java">public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程对象
        Producer producer = new Producer();
        Consumer consumer = new Consumer();
        //给线程设置名字
        producer.setName("生产者");
        consumer.setName("消费者");
        //开启线程
        producer.start();
        consumer.start();
    }
}

阻塞队列实现等待唤醒机制

何为阻塞队列?其实就是连接生产者和消费者的一个队列,管理着数据,分别供消费者take和生产者的put,如果put不进去或者take不出,则说明队列满了或者空了,这时候就会进入阻塞状态。

阻塞队列BlockingQueue本身实现了Iterable、Collection、Queue的接口,无法直接实例化,但是其具有2个实现类:

1、ArrayBlockingQueue:底层为数组,有界
2、LinkedBlockingQueue:底层为链表,无界(不是真正的无界,最大为int的最大范围,只是无须指定范围)

利用阻塞队列来实现是很便捷的,因为我们可以查看put和take方法的底层,可以发现这两个方法是自带锁的,所以我们在实现生产者和消费者的时候无须自己上锁,否则反而会容易因为锁的嵌套而发生死锁。
在这里插入图片描述
在这里插入图片描述
生产者代码:

java">public class Producer extends Thread{

    ArrayBlockingQueue<String> queue;

    public Producer(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            //直接不断的把数据放进阻塞队列,如果满了它自己会阻塞
            try {
                queue.put("数据");
                System.out.println("消费者生产了一个数据到阻塞队列");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

消费者代码:

java">public class Consumer extends Thread{

    ArrayBlockingQueue<String> queue;

    public Consumer(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            try {
                String take = queue.take();
                System.out.println(take);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

测试类:

java">public class ThreadDemo {
    /**
     * 使用阻塞队列实现等待唤醒机制,要保证生产者和消费者用的是同一个阻塞队列
     */
    public static void main(String[] args) {
        //创建一个可以存放1个数据的阻塞队列
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
        //创建生产者和消费者对象,并把阻塞队列传递过去,使得它们使用同一个阻塞队列
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);

        producer.setName("生产者");
        consumer.setName("消费者");

        producer.start();
        consumer.start();
    }
}

在这里插入图片描述
最后显示可能会重复打印数据,这是因为输出的语句没有放在锁里面,锁可以执行的put和take已经写死了,但是并不影响我们实际数据的并发安全性,只是不方便我们的观察罢了。

至此,阻塞队列实现等待唤醒机制的demo已经跑通了,阻塞队列底层的执行实际上是异步的,可以解决在实际生产环境中的超卖问题,具体可以看我之前的文章:
Redis:原理速成+项目实战——Redis实战9(秒杀优化)

当然,主流的方法还是使用消息队列RabbitMQ或Kafka,这个大家可以自行去了解。


http://www.niftyadmin.cn/n/5371146.html

相关文章

EasyX图形库学习(三、用easyX控制图形界面中的小球、图片-加载、输出)

目录 小球视频 图像输出函数 loadimage用于从文件中读取图片 putimage在当前设备上绘制指定图像。 initgraph 函数 图片输出 代码详解&#xff1a; 1. 初始化图形界面 2. 设置背景颜色并清除屏幕 3. 加载并显示图片 4. 等待用户输入并退出程序 图形界面中的小球 1…

yo!这里是Linux线程保姆级入门介绍

目录 前言 Linux线程基础 线程概念 底层示意图 线程vs进程 Linux线程控制 创建线程 线程ID 线程终止 线程等待 线程分离 Linux线程互斥 背景概念 互斥量mutex 1.相关接口 2.实现原理 可重入vs线程安全 死锁 Linux线程同步 条件变量 生产者消费者模型 基于…

【集合系列】HashMap 集合

HashMap 集合 1. 概述2. 方法3. 遍历方式4. 代码示例15. 代码示例26. 注意事项7. 源码分析 其他集合类 父类 Map 实现类 LinkedHashMap 集合类的遍历方式 具体信息请查看 API 帮助文档 1. 概述 HashMap 是 Java 中的一种集合类&#xff0c;它实现了 Map 接口。HashMap 使用键…

《低功耗方法学》翻译——附录B:UPF命令语法

附录B&#xff1a;UPF命令语法 本章介绍了文本中引用的所选UPF命令的语法。 节选自“统一电源格式&#xff08;UPF&#xff09;标准&#xff0c;1.0版”&#xff0c;经该Accellera许可复制。版权所有&#xff1a;(c)2006-2007。Accellera不声明或代表摘录材料的准确性或内容&…

openssl3.2 - exp - RAND_bytes_ex

文章目录 openssl3.2 - exp - RAND_bytes_ex概述笔记END openssl3.2 - exp - RAND_bytes_ex 概述 生成随机数时, 要检查返回值是否成功, 不能认为一定是成功的(官方文档上有说明). 生成随机数的API, 和库上下文有关系, 使用RAND_bytes_ex()比RAND_bytes()好些. 笔记 /*! * …

【芯片设计- RTL 数字逻辑设计入门 11.1 -- 状态机实现 移位运算与乘法 1】

文章目录 移位运算与乘法状态机简介SystemVerilog中的测试平台VCS 波形仿真 阻塞赋值和非阻塞赋值有限状态机&#xff08;FSM&#xff09;与无限状态机的区别 本篇文章接着上篇文章【芯片设计- RTL 数字逻辑设计入门 11 – 移位运算与乘法】 继续介绍&#xff0c;这里使用状态机…

基础图算法与社交网络分析

目录 前言1 寻找最短路径的Dijkstra算法1.1 介绍1.2 算法步骤1.3 应用领域1.4 算法优势与限制 2 构建高效网络结构的最小生成树算法2.1 Kruskal算法2.2 应用领域2.3 算法优势与限制 3 中心度算法3.1 PageRank算法3.2 Degree Centrality&#xff08;度中心度&#xff09;3.3 Bet…

C++算法之双指针、BFS和图论

一、双指针 1.AcWing 1238.日志统计 分析思路 前一区间和后一区间有大部分是存在重复的 我们要做的就是利用这部分 来缩短我们查询的时间 并且在使用双指针时要注意对所有的博客记录按时间从小到大先排好顺序 因为在有序的区间内才能使用双指针记录两个区间相差 相当于把一个…