当前位置: 首页 > article >正文

多线程案例——阻塞队列

目录

一、阻塞队列

1. 生产者消费者模型

(1)解耦合

(2)“削峰填谷”

2. 标准库中的阻塞队列

3. 自己实现一个阻塞队列(代码)

4. 自己实现生产者消费者模型(代码)


一、阻塞队列

   阻塞队列同样是一个符合先进先出的队列,相比于普通队列,阻塞队列还有其他的功能:

  • 1. 线程安全
  • 2. 产生阻塞效果
  • (1)如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止;
  • (2)如果队列满了,尝试入队列,就会出现阻塞,阻塞到队列不为满为止。

   基于上述特点的阻塞队列,我们就可以实现“生产者消费者模型”。(处理多线程问题的一个典型方式)。

1. 生产者消费者模型

   生产者消费者模型:通过一个容器(阻塞队列),来解决生产者消费者之间的强耦合问题。(生产者生产商品,消费者购买商品)

   开发实际生活中,使用的 阻塞队列 并不是一个简单的数据结构,而是一组专门的服务器程序。里面也不仅仅包含了阻塞队列的功能,还有其他的功能。这样的队列叫做 “消息队列” 

(1)解耦合

   假设现在有两个服务器 A 和 B ,A 作为入口服务器(直接接收用户的网络请求),B 作为应用服务器(给 A 提供服务)。

假设不使用 生产者消费者模型:

 即生产者和消费者之间直接进行沟通,则具有 强耦合性

强耦合性 体现在:

当开发 A 代码的时候就得充分了解 B 提供的一些接口,开发 B 代码的时候也得充分了解到 A 是怎么调用的。当我们想把 B 换成 C 时,A 的代码就得发生较大的改动,并且如果 B 崩溃了,也可能导致 A 也崩溃。 

   因此强耦合性是一种不好的现象。 而生产者消费者模型刚好可以降低这里的耦合,即模块和模块之间的联系尽可能的少。

   当 A 给 B 发送请求时,A 先将请求写到阻塞队列中,然后 B 再从阻塞队列中取出请求。当 B 返回结果时,先把结果放到阻塞队列中,然后 A 再从阻塞队列中取出结果。

  • 请求:A 是生产者,B 是消费者
  • 响应:A 是消费者,B 是生产者
  • 阻塞队列:作为交易场所

   使用了生产者消费者模型后,A 只需要关注如何和队列进行交互,不需要认识 B ,B 也是如此。当 B 崩溃时,对 A 没有影响。A 崩溃时, B 也没有影响。相应的,如果将 B 换做 C ,A 也完全感知不到。 就达到了解耦合的目的。

(2)“削峰填谷”

   使用生产者消费者模型,能够对于请求进行“削峰填谷”。(类似水库 -> 当雨季时,可以利用水库正常为稻田送水;当旱季时,打开水库,同样可以保证为稻田正常送水)

   还是使用上述 A 和 B 服务器的例子。假设我们没有使用生产者消费者模型:

削峰:

   当有大量请求时,A 服务器作为入口服务器,计算量轻,不会造成太大影响。而因为强耦合性,B 也会突然暴涨,B 作为应用服务器,计算量比 A 大得多,可能就直接造成崩溃了。

   如果使用了生产者消费者模型,遇到请求暴涨的情况:

   A 的请求暴涨,导致了阻塞队列的暴涨,因为阻塞队列只是存储数据,因此没有太大影响。B 这边依然按照原来的速度消费数据,不会因为 A 的暴涨而引起暴涨。因此 B 就被保护起来了,不会因为这种请求暴涨而崩溃,即 削峰 。(让 B 感受不到)

填谷:

   当 A 的请求很少,而 B 依然会按照原来的速度消费数据。虽然 A 的请求少,但是阻塞队列中还有挤压的数据,就能够让 B 正常运行。

2. 标准库中的阻塞队列

   Java标准库中自带了阻塞队列,当我们编写代码时需要使用阻塞队列时,直接使用即可。

  • BlockingQueue 是一个接口,真正实现的类时 LinkedBlockingQueue。(类似List,LinkedList和ArrayList)
  • put:阻塞式出队列
  • take:阻塞式出队列
  • (也有 offer、poll、peek等普通队列有的方法,但是这些方法不带有阻塞特性)
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new LinkedBlockingDeque<>();
        //阻塞式入队列,如果队列满,就会阻塞
        queue.put("jiu");
        //阻塞式出队列,如果队列空,就会阻塞
        System.out.println(queue.take());   //结果为 jiu
    }
}

3. 自己实现一个阻塞队列(代码)

   要实现一个阻塞队列:一个普通队列 + 线程安全 + 阻塞功能 。

① 实现队列(先进先出)可以通过链表或者数组,这里使用数组。——> 循环队列(队首head 和 队尾 tail)

  • 入队列:把新元素放到 tail 的位置,tail++
  • 出队列:取出 head 位置的元素,head++
  • 循环队列:当 head 或者 tail 到达数组末尾时,就需要从头开始(= 0),重新循环。
  • 判空 / 满:要实现循环队列,这是一个重要问题。我们用 变量 size 记录元素的个数,进行判断。

② 实现线程安全。因为在方法中全是共享变量,所以可以直接对整个方法进行加锁,也可以对整个代码块进行加锁,同时专门设置一个锁对象locker。

③ 实现阻塞效果。利用 wait 和 notify 来实现。使用哪个对象加锁就要用哪个对象 wait

  • 对于 put 来说,阻塞条件:队列为满。而 put 中的 wait 要由 take 来唤醒。(take 成功了队列就不满了)
  • 对于 take 来说,阻塞条件:队列为空。而 take 中的 wait 要由 put 来唤醒。(put 成功了队列就不为空了)

   如果有人等待,notify 可以唤醒;如果没人等待,notify 也没有任何影响。

//假设队列中存放的是整型元素
class MyBlockingQueue {
    private int[] array = new int[1000];
    private int size = 0;
    private int head = 0;
    private int tail = 0;

    private Object locker = new Object();

    //阻塞式入队列
    public void put(int value) throws InterruptedException {
        synchronized(locker) {
            if (size == array.length) {
                //说明队列满了,无法再入队列了
                //所以进入阻塞状态
                locker.wait();
            }
            //队列没有满,可以入队列
            //1.把值放到 tail 的位置
            array[tail] = value;
            tail++;
            //2.判断是否到队尾的情况(等价于 tail = tail % array.length)
            if (tail >= array.length) {
                tail = 0;
            }
            //3.size++
            size++;
            //入队列成功,队列非空,从阻塞队列中唤醒 take
            locker.notify();
        }

    }

    //阻塞式出队列
    public int take() throws InterruptedException {
        synchronized(locker) {
            if (size == 0) {
                //队列为空,没有元素可以出
                //进入阻塞状态
                locker.wait();
            }
            //队列中有元素,可以出队列
            //1.取出 head 位置的值
            int ans = array[head];
            head++;
            //2. 判断是否到队尾
            if (head >= array.length) {
                head = 0;
            }
            //3. size--
            size--;
            //出队列成功,队列不为满,从阻塞队列中唤醒 put
            locker.notify();
            return ans;
        }

    }
}

4. 自己实现生产者消费者模型(代码)

public class Demo2 {
    //自己实现生产者消费者模型
    public static MyBlockingQueue queue = new MyBlockingQueue();
    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            int num = 0;
            while(true) {
                try {
                    queue.put(num);
                    System.out.println("生产了: " + num);
                    num++;
                    //让生产者生产慢一点,消费者就得跟着生产者的步伐
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        Thread customer = new Thread(() -> {
           while(true) {
               try {
                   int num = queue.take();
                   System.out.println("消费了: " + num);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        customer.start();
    }
}


http://www.kler.cn/a/866.html

相关文章:

  • Unity 3D游戏开发从入门进阶到高级
  • <代码随想录> 算法训练营-2025.01.09
  • unity——Preject3——面板基类
  • FPGA EDA软件的位流验证
  • day08_Kafka
  • ORACLE-表空间和分区控制
  • HTTP详解
  • 15000 字的 SQL 语句大全 第一部分
  • C语言格式和注意点
  • Redis知识点汇总
  • <Linux>计算机体系结构和操作系统
  • 我一个女孩子居然做了十年硬件……
  • Qss样式表语法
  • JavaScript 库
  • 数影周报:SpaceX设计图纸被泄露,拍明芯城正式在纳斯达克上市
  • 【YOLOv8/YOLOv7/YOLOv5/YOLOv4/Faster-rcnn系列算法改进NO.60】损失函数改进为wiou
  • 计算机网络的基本组成
  • 【笔试强训选择题】Day3.习题(错题)解析
  • 什么是PCB走线的3W原则
  • 用ChatGPT编写的一个调用ElasticSearch的maven的spring elasticsearch demo案例
  • 面试——Java基础
  • 前端html知识看完这篇博客就够了
  • 基础IO【Linux】
  • python打包成apk界面设计,python打包成安装文件
  • 顺序表——“数据结构与算法”
  • ChatGPT研究分析:GPT-4做了什么