当前位置:

首页 > 编程开发 > 怎么利用Java手写阻塞队列

怎么利用Java手写阻塞队列

需求分析阻塞队列的主要的需求如下:队列基础的功能需要有,往队列当中放数据,从队列当中取数据。所有的队列操作都要是并发安全的。当队列满了之后再往队列当中放数据的时候,线程需要被挂起,当队列当中的数据被取出,让队列当中有空间的时候线程需要被唤醒。当队列空了之后再往队列当中取数据的时候,线程需要被挂起,当有线程往队列当中加入数据的时候被挂起的线程需要被唤醒。在我们实现的队列当中我们使用数组去存储数据,因此在构造函数当中需要提供数组的初始大小,设置用多大的数组。阻塞队列实现原理线程阻塞和唤醒在上面我们已经谈到了阻

    需求分析

    阻塞队列的主要的需求如下:

    • 队列基础的功能需要有,往队列当中放数据,从队列当中取数据。

    • 所有的队列操作都要是并发安全的。

    • 当队列满了之后再往队列当中放数据的时候,线程需要被挂起,当队列当中的数据被取出,让队列当中有空间的时候线程需要被唤醒。

    • 当队列空了之后再往队列当中取数据的时候,线程需要被挂起,当有线程往队列当中加入数据的时候被挂起的线程需要被唤醒。

    • 在我们实现的队列当中我们使用数组去存储数据,因此在构造函数当中需要提供数组的初始大小,设置用多大的数组。

    阻塞队列实现原理

    线程阻塞和唤醒

    在上面我们已经谈到了阻塞队列是并发安全的,而且我们还有将线程唤醒和阻塞的需求,因此我们可以选择可重入锁ReentrantLock保证并发安全,但是我们还需要将线程唤醒和阻塞,因此我们可以选择条件变量Condition进行线程的唤醒和阻塞操作,在Condition当中我们将会使用到的,主要有以下两个函数:

    • signal用于唤醒线程,当一个线程调用Conditionsignal函数的时候就可以唤醒一个被await函数阻塞的线程。

    • await用于阻塞线程,当一个线程调用Conditionawait函数的时候这个线程就会阻塞。

    数组循环使用

    因为队列是一端进一端出,因此队列肯定有头有尾。

    怎么利用Java手写阻塞队列

    当我们往队列当中加入一些数据之后,队列的情况可能如下:

    怎么利用Java手写阻塞队列

    在上图的基础之上我们在进行四次出队操作,结果如下:

    怎么利用Java手写阻塞队列

    在上面的状态下,我们继续加入8个数据,那么布局情况如下:

    怎么利用Java手写阻塞队列

    我们知道上图在加入数据的时候不仅将数组后半部分的空间使用完了,而且可以继续使用前半部分没有使用过的空间,也就是说在队列内部实现了一个循环使用的过程。

    为了保证数组的循环使用,我们需要用一个变量记录队列头在数组当中的位置,用一个变量记录队列尾部在数组当中的位置,还需要有一个变量记录队列当中有多少个数据。

    代码实现

    成员变量定义

    根据上面的分析我们可以知道,在我们自己实现的类当中我们需要有如下的类成员变量:

    // 用于保护临界区的锁
    private final ReentrantLock lock;
    // 用于唤醒取数据的时候被阻塞的线程
    private final Condition notEmpty;
    // 用于唤醒放数据的时候被阻塞的线程
    private final Condition notFull;
    // 用于记录从数组当中取数据的位置 也就是队列头部的位置
    private int takeIndex;
    // 用于记录从数组当中放数据的位置 也就是队列尾部的位置
    private int putIndex;
    // 记录队列当中有多少个数据
    private int count;
    // 用于存放具体数据的数组
    private Object[] items;

    构造函数

    我们的构造函数也很简单,最核心的就是传入一个数组大小的参数,并且给上面的变量进行初始化赋值。

    @SuppressWarnings("unchecked")
    public MyArrayBlockingQueue(int size) {
      this.lock = new ReentrantLock();
      this.notEmpty = lock.newCondition();
      this.notFull = lock.newCondition();
      // 其实可以不用初始化 类会有默认初始化 默认初始化为0
      takeIndex = 0;
      putIndex = 0;
      count = 0;
      // 数组的长度肯定不能够小于0
      if (size <= 0)
        throw new RuntimeException("size can not be less than 1");
      items = (E[])new Object[size];
    }

    put函数

    这是一个比较重要的函数了,在这个函数当中如果队列没有满,则直接将数据放入到数组当中即可,如果数组满了,则需要将线程挂起。

    public void put(E x){
      // put 函数可能多个线程调用 但是我们需要保证在给变量赋值的时候只能够有一个线程
      // 因为如果多个线程同时进行赋值的话 那么可能后一个线程的赋值操作覆盖了前一个线程的赋值操作
      // 因此这里需要上锁
      lock.lock();
     
      try {
        // 如果队列当中的数据个数等于数组的长度的话 说明数组已经满了
        // 这个时候需要将线程挂起
        while (count == items.length)
          notFull.await(); // 将调用 await的线程挂起
        // 当数组没有满 或者在挂起之后再次唤醒的话说明数组当中有空间了
        // 这个时候需要将数组入队 
        // 调用入队函数将数据入队
        enqueue(x);
      } catch (InterruptedException e) {
        e.printStackTrace();
      } finally {
        // 解锁
        lock.unlock();
      }
    }
     
    // 将数据入队
    private void enqueue(E x) {
      this.items[putIndex] = x;
      if (++putIndex == items.length)
        putIndex = 0;
      count++;
      notEmpty.signal(); // 唤醒一个被 take 函数阻塞的线程唤醒
    }

    offer函数

    offer函数和put函数一样,但是与put函数不同的是,当数组当中数据填满之后offer函数返回false,而不是被阻塞。

    public boolean offer(E e) {
      final ReentrantLock lock = this.lock;
      lock.lock();
      try {
        // 如果数组满了 则直接返回false 而不是被阻塞
        if (count == items.length)
          return false;
        else {
          // 如果数组没有满则直接入队 并且返回 true
          enqueue(e);
          return true;
        }
      } finally {
        lock.unlock();
      }
    }

    add函数

    这个函数和上面两个函数作用一样,也是往队列当中加入数据,但当单队列满了之后这个函数会抛出异常。

    public boolean add(E e) {
      if (offer(e))
        return true;
      else
        throw new RuntimeException("Queue full");
    }

    take函数

    这个函数主要是从队列当中取出一个数据,但是当队列为空的时候,这个函数会阻塞调用该函数的线程:

    public E take() throws InterruptedException {
      // 这个函数也是不能够并发的 否则可能不同的线程取出的是同一个位置的数据
      // 进行加锁操作
      lock.lock();
      try {
        // 当 count 等于0 说明队列为空
        // 需要将线程挂起等待
        while (count == 0)
          notEmpty.await();
        // 当被唤醒之后进行出队操作
        return dequeue();
      }finally {
        lock.unlock();
      }
    }
     
    private E  dequeue() {
      final Object[] items = this.items;
      @SuppressWarnings("unchecked")
      E x = (E) items[takeIndex];
      items[takeIndex] = null; // 将对应的位置设置为 null GC就可以回收了
      if (++takeIndex == items.length)
        takeIndex = 0;
      count--; // 队列当中数据少一个了
      // 因为出队了一个数据 可以唤醒一个被 put 函数阻塞的线程 如果这个时候没有被阻塞的线程
      // 这个函数就不会起作用 也就说在这个函数调用之后被 put 函数挂起的线程也不会被唤醒
      notFull.signal(); // 唤醒一个被 put 函数阻塞的线程
      return x;
    }

    重写toString函数

    因为我们在后面的测试函数当中会打印我们这个类,而打印这个类的时候会调用对象的toString方法得到一个字符串,最后打印这个字符串。

    @Override
    public String toString() {
      StringBuilder stringBuilder = new StringBuilder();
      stringBuilder.append("[");
      // 这里需要上锁 因为我们在打印的时候需要打印所有的数据
      // 打印所有的数据就需要对数组进行遍历操作 而在进行遍历
      // 操作的时候是不能进行插入和删除操作的 因为打印的是某
      // 个时刻的数据
      lock.lock();
      try {
        if (count == 0)
          stringBuilder.append("]");
        else {
          int cur = 0;
          // 对数据进行遍历 一共遍历 count 次 因为数组当中一共有 count
          // 个数据
          while (cur != count) {
            // 从 takeIndex 位置开始进行遍历 因为数据是从这个位置开始的
            stringBuilder.append(items[(cur + takeIndex) % items.length].toString() + ", ");
            cur += 1;
          }
          // 删除掉最后一次没用的 ", "
          stringBuilder.delete(stringBuilder.length() - 2, stringBuilder.length());
          stringBuilder.append(']');
        }
      }finally {
        lock.unlock();
      }
      return stringBuilder.toString();
    }

    完整代码

    整个我们自己完成的阻塞队列的代码如下:

    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
     
    public class MyArrayBlockingQueue {
     
      // 用于保护临界区的锁
      private final ReentrantLock lock;
      // 用于唤醒取数据的时候被阻塞的线程
      private final Condition notEmpty;
      // 用于唤醒放数据的时候被阻塞的线程
      private final Condition notFull;
      // 用于记录从数组当中取数据的位置 也就是队列头部的位置
      private int takeIndex;
      // 用于记录从数组当中放数据的位置 也就是队列尾部的位置
      private int putIndex;
      // 记录队列当中有多少个数据
      private int count;
      // 用于存放具体数据的数组
      private Object[] items;
     
     
      @SuppressWarnings("unchecked")
      public MyArrayBlockingQueue(int size) {
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.notFull = lock.newCondition();
        // 其实可以不用初始化 类会有默认初始化 默认初始化为0
        takeIndex = 0;
        putIndex = 0;
        count = 0;
        if (size <= 0)
          throw new RuntimeException("size can not be less than 1");
        items = (E[])new Object[size];
      }
     
      public void put(E x){
        lock.lock();
     
        try {
          while (count == items.length)
            notFull.await();
          enqueue(x);
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          lock.unlock();
        }
      }
     
      private void enqueue(E x) {
        this.items[putIndex] = x;
        if (++putIndex == items.length)
          putIndex = 0;
        count++;
        notEmpty.signal();
      }
     
      private E  dequeue() {
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
          takeIndex = 0;
        count--;
        notFull.signal();
        return x;
      }
     
      public boolean add(E e) {
        if (offer(e))
          return true;
        else
          throw new RuntimeException("Queue full");
      }
     
      public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
          if (count == items.length)
            return false;
          else {
            enqueue(e);
            return true;
          }
        } finally {
          lock.unlock();
        }
      }
     
      public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
          return (count == 0) ? null : dequeue();
        } finally {
          lock.unlock();
        }
      }
     
      public E take() throws InterruptedException {
        lock.lock();
        try {
          while (count == 0)
            notEmpty.await();
          return dequeue();
        }finally {
          lock.unlock();
        }
      }
     
      @Override
      public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("[");
        lock.lock();
        try {
          if (count == 0)
            stringBuilder.append("]");
          else {
            int cur = 0;
            while (cur != count) {
              stringBuilder.append(items[(cur + takeIndex) % items.length].toString()).append(", ");
              cur += 1;
            }
            stringBuilder.delete(stringBuilder.length() - 2, stringBuilder.length());
            stringBuilder.append(']');
          }
        }finally {
          lock.unlock();
        }
        return stringBuilder.toString();
      }
     
    }

    现在对上面的代码进行测试:

    我们现在使用阻塞队列模拟一个生产者消费者模型,设置阻塞队列的大小为5,生产者线程会往队列当中加入数据,数据为0-9的10个数字,消费者线程一共会消费10次。

    import java.util.concurrent.TimeUnit;
     
    public class Test {
     
      public static void main(String[] args) throws InterruptedException {
        MyArrayBlockingQueue queue = new MyArrayBlockingQueue<>(5);
        Thread thread = new Thread(() -> {
          for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " 往队列当中加入数据:" + i);
            queue.put(i);
          }
        }, "生产者");
     
     
        Thread thread1 = new Thread(() -> {
          for (int i = 0; i < 10; i++) {
            try {
              System.out.println(Thread.currentThread().getName() + " 从队列当中取出数据:" + queue.take());
              System.out.println(Thread.currentThread().getName() + " 当前队列当中的数据:" + queue);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }, "消费者");
        thread.start();
        TimeUnit.SECONDS.sleep(3);
        thread1.start();
     
      }
    }

    上面代码的输出如下所示:

    生产者 往队列当中加入数据:0
    生产者 往队列当中加入数据:1
    生产者 往队列当中加入数据:2
    生产者 往队列当中加入数据:3
    生产者 往队列当中加入数据:4
    生产者 往队列当中加入数据:5
    消费者 从队列当中取出数据:0
    生产者 往队列当中加入数据:6
    消费者 当前队列当中的数据:[1, 2, 3, 4, 5]
    消费者 从队列当中取出数据:1
    消费者 当前队列当中的数据:[2, 3, 4, 5]
    消费者 从队列当中取出数据:2
    消费者 当前队列当中的数据:[3, 4, 5, 6]
    生产者 往队列当中加入数据:7
    消费者 从队列当中取出数据:3
    消费者 当前队列当中的数据:[4, 5, 6, 7]
    消费者 从队列当中取出数据:4
    消费者 当前队列当中的数据:[5, 6, 7]
    消费者 从队列当中取出数据:5
    消费者 当前队列当中的数据:[6, 7]
    生产者 往队列当中加入数据:8
    消费者 从队列当中取出数据:6
    消费者 当前队列当中的数据:[7, 8]
    消费者 从队列当中取出数据:7
    消费者 当前队列当中的数据:[8]
    消费者 从队列当中取出数据:8
    消费者 当前队列当中的数据:[]
    生产者 往队列当中加入数据:9
    消费者 从队列当中取出数据:9
    消费者 当前队列当中的数据:[]

    从上面的输出结果我们知道,生产者线程打印5之后被挂起了,因为如果没有被挂起,生产者线程肯定可以一次性输出完成,因为消费者线程阻塞了3秒。但是他没有输出完成说明在打印5之后,因为阻塞队列满了,因而生产者线程被挂起了。然后消费者开始消费,这样阻塞队列当中就有空间了,生产者线程就可以继续生产了。

    本文内容来源于互联网,如有侵权请联系删除。
    作者最新文章
    编程开发
    相关文章 更多
    谷歌浏览器Mac版入口
    谷歌浏览器Mac版入口

    谷歌浏览器Mac版官方安装指南 谷歌浏览器Mac版官方安装入口是https://www.google.com/chrome/,需macOS 12+系统、500MB空间,下载.dmg后拖入应用程序安装,支持多设备同步、性能优化与隐私保护功能。 苹果电脑Chrome的安装入口究竟在哪里?这个问题最近可是

    Chrome浏览器JS脚本不运行怎么办
    Chrome浏览器JS脚本不运行怎么办

    Chrome中JavaScript未执行需依次检查:一、移除站点级禁用并添加允许域名;二、开启全局JavaScript开关;三、禁用干扰扩展;四、在开发者工具中启用JavaScript;五、重置内容设置为默认。 有时在Chrome里打开网页,会发现交互按钮点了没反应,数据加载不出来,页面仿佛“静止”

    IE浏览器怀旧版在线网址
    IE浏览器怀旧版在线网址

    IE浏览器怀旧版在线网址:一次精准的技术时光回溯 最近,不少老用户和怀旧爱好者在反复搜索一个问题:那个经典的Internet Explorer,如今还能在哪里原汁原味地体验到?答案指向一个特定的地址:https://ie.microsoft.com/legacy/。 这个网站远不止是一个简单的“皮肤

    火狐浏览器有哪些设置功能
    火狐浏览器有哪些设置功能

    火狐浏览器五大核心设置功能:解锁高效、安全与个性化体验 火狐浏览器功能强大,但如果不仔细挖掘,很多能大幅提升效率和安全性的设置可能就“藏着掖着”了。这就好比拥有一台高性能设备,却只用了基础模式。那么,如何把它调整到最顺手、最安全的状态?接下来,我们就聚焦于当前版本(截至2025年末)最关键的五大设置

    chrome搜索免验证入口
    chrome搜索免验证入口

    Chrome官方免验证入口为https://www.google.cn/chrome/,提供全平台安装包、免登录即用、本地化安全机制及引擎级性能优化。 到底该去哪里找正版、免费且无需繁琐验证的Chrome浏览器入口?这个问题困扰了不少网友。今天,我们就来直通核心,为大家详细拆解Chrome引擎的官方

    java heap space 选型思路:使用场景与区别整理
    java heap space 选型思路:使用场景与区别整理

    Java堆是JVM存储对象的核心内存区域,配置需结合场景:单体应用适中设置;大数据处理需大堆并关注GC停顿;微服务强调快速启动;高并发需精细划分堆区域。关键参数-Xms和-Xmx建议等值以稳定性能。垃圾回收器选择影响效率,如G1适用于大堆,ZGC可实现低停顿。内存错误时需监控堆状态。

    java heap space 使用中遇到的问题怎么解决
    java heap space 使用中遇到的问题怎么解决

    Java堆内存溢出错误通常因内存泄漏、数据处理需求过大或JVM参数配置不当引起。排查时可借助jmap、堆转储及MAT等工具定位问题。解决方案包括调整JVM内存参数(如-Xmx)、修复代码中的内存泄漏、优化大数据处理逻辑,并建立持续监控与预防机制,以保障应用稳定运行。

    java xml 选型思路:使用场景与区别整理
    java xml 选型思路:使用场景与区别整理

    XML在Java开发中用于配置、数据交换等场景。解析方式主要有DOM、SAX、StAX及第三方库。DOM适合操作小文件,SAX/StAX适合处理大文件流,JAXB用于对象与XML映射。选型需结合数据大小、内存、性能及团队熟悉度,现代框架常封装底层解析。

    using namespace 使用中遇到的问题怎么解决
    using namespace 使用中遇到的问题怎么解决

    命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

    c语言函数递归 实操经验总结:这些技巧很实用
    c语言函数递归 实操经验总结:这些技巧很实用

    理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    macOS软件
    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    Mac软件 更多
    灵活计算器
    灵活计算器
    macOS/iOS/Android

    灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    赤友清理大师
    赤友清理大师
    macOS

    赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    WINDOWS 更多
    Windows 10
    Windows 10
    Windows

    Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    密码键盘
    密码键盘
    Windows/macOS/iOS/Android

    密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。