当前位置:

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

怎么利用Java手写阻塞队列

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

本文目录

      需求分析

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

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

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

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

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

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

      阻塞队列实现原理

      线程阻塞和唤醒

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

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

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

      数组循环使用

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

      怎么利用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之后,因为阻塞队列满了,因而生产者线程被挂起了。然后消费者开始消费,这样阻塞队列当中就有空间了,生产者线程就可以继续生产了。

      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发 Java
      相关文章 更多
      Java单元测试入门:使用JUnit 5编写第一个测试用例
      Java单元测试入门:使用JUnit 5编写第一个测试用例

      面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

      Java变量未初始化报错解析:局部变量与成员变量默认值差异
      Java变量未初始化报错解析:局部变量与成员变量默认值差异

      深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

      详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

      Java中final与static final常量的定义及初始化规则
      Java中final与static final常量的定义及初始化规则

      深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

      Java抽象方法与接口方法的区别及选型建议
      Java抽象方法与接口方法的区别及选型建议

      深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

      Java实现HTTP请求与JSON解析的完整代码示例
      Java实现HTTP请求与JSON解析的完整代码示例

      本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

      Java递归出现StackOverflowError的原因分析及栈空间优化方案
      Java递归出现StackOverflowError的原因分析及栈空间优化方案

      详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

      Java空指针异常(NPE)的代码排查与修复实例
      Java空指针异常(NPE)的代码排查与修复实例

      针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

      selenium上传文件教程本地文件路径设置方法
      selenium上传文件教程本地文件路径设置方法

      深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

      针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

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

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

      Windows
      Windows

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

      PDF教程
      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版
      macOS

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版
      Windows

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。