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

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

生产者-消费者模型:

在计算机科学中,生产者-消费者模型是一种经典的多线程/多进程同步问题 。它描述了一组生产者线程和一组消费者线程,它们通过一个**共享的、固定大小的缓冲区(通常是 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,线程继续往下执行,但别忘了,此时队列是满的,塞不进了,所以往下执行势必会产生一些安全问题

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

相关推荐
Tian_Hang3 小时前
Linux基础知识(四)
linux·ide·驱动开发·计算机视觉·硬件工程·动画
夕除3 小时前
AOP 实现 Redis 缓存切面解析
java·开发语言·python
库拉大叔3 小时前
工具调用效率对比实测:GPT-5.5与Gemini 3.5 Flash性能评估
java·前端·人工智能
我是唐青枫3 小时前
Java MyBatis 实战指南:XML 映射、动态 SQL 与数据访问层设计
java·mybatis
摇滚侠3 小时前
Spring 零基础入门到进阶 面向切面 AOP 52-60
java·后端·spring
就改了3 小时前
微服务接口性能优化:CompletableFuture 并行聚合实践
java·微服务·性能优化
TechWayfarer3 小时前
IP画像在企业安全中的应用:它能做什么?不能替代什么
网络·python·tcp/ip·安全·网络安全
林森lsjs3 小时前
【日耕一题】4. 较为复杂情况下的求和
java·开发语言
Hui Baby3 小时前
虚拟线程整理
java