以“轮盘数组”思维彻底搞懂并实现阻塞队列

要想学习并实现阻塞队列,这里我们要先搞懂什么是生产者-消费者模型

生产者-消费者模型:

在计算机科学中,生产者-消费者模型是一种经典的多线程/多进程同步问题 。它描述了一组生产者线程和一组消费者线程,它们通过一个**共享的、固定大小的缓冲区(通常是 FIFO 队列)**进行通信和协作

在这个模型中,有三个核心组成部分和三种关键关系:

  • 生产者(Producer):生成数据或任务,并将其放入共享缓冲区。如果缓冲区满了,它必须等待,直到缓冲区中有空位出现
  • 消费者(Consumer):从缓冲区中取出数据并进行处理。如果缓冲区空了,它必须等待,直到缓冲区中有任务存在
  • 缓冲区(Buffer):作为中间的通信通道,屏蔽了生产者和消费者的细节。对它的操作必须是线程安全的

具体了解缓冲区是什么,有什么用:

生产者生产任务,消费者负责消耗任务,这很好理解,那么什么是缓冲区呢?我想以一个简单的例子为例:

我们可以将这个模型结合"三峡大坝"来看,来看看缓冲区的作用是什么

我们先介绍三峡大坝体系中的生产者与消费者:

·生产者: 在长江上游,江水(数据/任务)时而汹涌澎湃,时而平缓流淌,这就像我们的**生产者线程,**是不稳定的

·消费者: 而在大坝下游,发电机组需要以恒定的速度转动发电,这就像我们的消费者线程,处理能力往往是有限且固定的

想要知道缓冲区(也就是大坝)的作用是什么,我们可以观察若没有缓冲区存在会出现什么情况:

  • 洪水期(生产过快): 下游的发电机根本来不及处理巨大的水量,如果不加控制,下游就会发生洪涝灾害(内存溢出或系统崩溃)
  • 枯水期(生产过慢): 上游没水下来,发电机只能空转甚至停机等待(线程频繁阻塞唤醒,浪费 CPU 资源)

而缓冲区(阻塞队列),就是这座调节水位的"大坝水库"

它作为一个缓冲区,完美地解决了上下游速率不匹配的问题:

  1. 削峰: 当上游来水太急时,先把水蓄在水库里(入队),等下游慢慢处理。
  2. 填谷: 当上游没水时,释放库存的水让下游继续发电(出队),保证系统平稳运行。

这种机制不仅保护了下游的消费者不被冲垮,还实现了生产者和消费者的解耦------他们不需要知道对方的存在,只需要对着大坝操作即可。

那么,Java 是如何在底层实现这样一个精密的"数字大坝"的呢?其实,它的核心构造往往依赖于一种特殊的数组结构。这就不得不提我们之前深入探讨过的 轮盘数组,它是构建高效环形缓冲区的基石......

阻塞队列是一个以轮盘数组为骨架,以锁为灵魂的工具,接下来我们带着轮盘数组的思想来实现一个阻塞队列。

一、基础成员和构造方法

首先,我们实现基础成员和构造方法,我们以String类型为例

复制代码
class MyBlockingQueue {

    private String data[] = null;

    public MyBlockingQueue(int n) {
        this.data = new String[n];
    }

    private int size = 0;

    private int head;

    private int last;
}

由于接下来的put和take方法都会涉及到改,而改则会在多线程环境下产生线程安全问题,所以我们一定要记得加锁,这里我们以this作为锁

二、put(String elem)

复制代码
public void put(String elem) throws InterruptedException {
   //修改数组操作,多线程情况下必须加锁
    synchronized (this){
        while(size== data.length){
            this.wait();
        }
        data[last] = elem;
        size++;
        last = (last+1)%data.length;
        this.notify();
    }
}

三、take()

复制代码
public String take() throws InterruptedException {
       synchronized (this){
           while(size==0){
               this.wait();
           }
           String ret = data[head];
           size--;
           head = (head+1)%data.length;
           this.notify();
           return ret;
       }
}

⭐为什么我们不写成if而是要写成while循环判断队列的具体情况?

这里就要讲到为什么 wait()等待一定要配合while循环使用了:

·我们的wait等待不是一定只能有notify唤醒,还可能被Interrupt这样的方法给中断,如果使用if作为wait的判定条件,此时就存在wait被提前唤醒的风险,导致队列明明是(空/满),但我们还是执行了(put/take)方法,这会导致一些安全问题

⭐为了进一步解释为什么wait要配合while循环使用,接下来我将介绍一种极端情况

我们有一个大小为3的阻塞队列,此时队列里已经有3个元素了,但此时有三个线程(t1,t2,t3)都调用了put方法,那么这三个线程一定都会阻塞,如果此时我们take()出了一个元素,那么take方法中的notify会随机唤醒一个正在因调用put方法且队列为满而阻塞的线程,我们设这个线程为t1,t1又put进一个元素,那么此时队列又为满了,但是t1调用的put方法中也有一个notify,那么这个notify很可能会去随机唤醒23线程中的随机一个,如果put方法中的while循环此时为if,那么就会唤醒if语句里的wait,线程继续往下执行,但别忘了,此时队列是满的,塞不进了,所以往下执行势必会产生一些安全问题

这样我们就成功实现了一个阻塞队列

相关推荐
智塑未来3 小时前
中小公司在线文档选型:从协作入口到安全边界
运维·安全
山东科恩光电4 小时前
提升橡塑行业安全性,科恩密佑KKL-01光敏传感器的应用优势与前景
安全
Julien20044 小时前
控制 SELinux 文件上下文
linux·运维·服务器
EXI-小洲4 小时前
Java 操作 Word:字符串替换、图片插入、动态生成表格与API接口下载
java·开发语言·spring boot·word
sulikey4 小时前
Linux Core Dump 完全指南:从原理到实战的崩溃调试手册。什么是core dump?程序报错core dumped怎么办?
linux·服务器·操作系统·调试·coredump
Lifangyun_WD4 小时前
第一次租 GPU 服务器怎么开始?镜像、SSH、数据存储和停机前要做什么
服务器·云计算·gpu算力·gpu算力租赁
2601_961901705 小时前
SpringBoot使用Nacos进行application.yml配置管理
java·spring boot·spring
是隼人5 小时前
buuctf-pwn xdctf2015_pwn200题解(学习过程持续更新)
c语言·学习·安全·pwn入门·ctf入门
DevLoom_5 小时前
SD卡插电脑提示:驱动器中的磁盘未被格式化?恢复数据正确处理步骤
运维·服务器·电脑
大衛說6 小时前
02 · 环境搭建:uv 包管理与现代 IDE
ide·uv