Java 中的阻塞队列

2023-11-06

Java 中的阻塞队列:

1. ArrayBlockingQueue :由数组结构组成的有界阻塞队列。

2. LinkedBlockingQueue :由链表结构组成的有界阻塞队列。

3. PriorityBlockingQueue :支持优先级排序的无界阻塞队列。

4. DelayQueue:使用优先级队列实现的无界阻塞队列。

5. SynchronousQueue:不存储元素的阻塞队列。

6. LinkedTransferQueue:由链表结构组成的无界阻塞队列。

7. LinkedBlockingDeque:由链表结构组成的双向阻塞队列

ArrayBlockingQueue(公平、非公平)

用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,

当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。

我们可以使用以下代码创建一个公平的阻塞队列:

ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

ArrayBlockingQueue的主要属性有:

    //存储元素的数组
    final Object[] items;

    //下一个出队的索引
    int takeIndex;

    //下一个入队的索引
    int putIndex;

    //队列的大小
    int count;

    //可重入锁
    final ReentrantLock lock;

    //出队操作的等待
    private final Condition notEmpty;

    //入队操作的等待
    private final Condition notFull;

实现原理:通过可重入锁ReenTrantLock+Condition 来实现多线程之间的同步效果

入队过程:

  • add方法:插入成功返回true;插入失败抛异常
  • put方法:插入元素到尾部,如果失败则调用Condition.await()方法进行阻塞等待,直到被唤醒;
  • offer方法:插入元素到尾部,如果失败则直接返回false,
  • offer(timeout):插入元素到尾部,如果失败则调用Condition.await(timeout)方法进行阻塞等待指定时间,直到被唤醒或阻塞超时,还是失败就返回false
  • 而一旦插入成功,就会唤醒出队的等待操作,执行出队的Condition的signal()方法

出队过程:

  • 主要方法为:poll()、take()、remove()
  • 基本上和入队过程类似,出队结束会唤醒入队的等待操作,执行入队的Condition的signal()方法
  • 而不管是入队操作还是出队操作,都会通过ReentrantLock来控制同步效果,通过两个Condition来控制线程之间的通信效果
  • 另外入队和出队操作分别通过两个索引 takeIndex 和putIndex来指定数组的位置,默认从0开始分别递增,如果达到数组的容量大小,就表示到了数组的边界了,此时再设置index=0,相当于数组是一个环形数组
  • 环形数组的好处是增删数据时不需要挪动数组中的其他数据,只需要改变入队和出队的指针即可。而如果不是环形数组而是顺序数组的话,入队和出队就需要大量移动数据,否则数组空间一下就被用完了,性能较差

环形数组简单实现:

        class CircleQueue {
            private int front;  // 指向队列头
            private int rear;   // 指向队列尾部位置的下一个,当队列满的时候,队列第一个数据和最后一个之间有一个空的位置
            private int maxsize;
            private int[] arr;

            public CircleQueue(int maxsize) {
                this.maxsize = maxsize;
                arr = new int[this.maxsize];
            }

            // 队列是否为空
            public boolean isEmpty() {
                return front == rear;
            }

            // 判断是否满
            public boolean isFull() {
                return (rear + 1) % maxsize == front;
            }

            // 添加数据
            public void add(int value) {
                if (isFull()) {
                    throw new RuntimeException("the queue is full!");
                }
                arr[rear] = value;
                rear = (rear + 1) % maxsize;
            }

            // 取出数据
            public int get() {
                if (isEmpty()) {
                    throw new RuntimeException("the queue is full!");
                }
                int temp = arr[front];
                front = (front + 1) % maxsize;
                return temp;
            }

            // 存有多少数据
            public int size() {
                return (rear + maxsize - front) % maxsize;
            }

            // 显示数据
            public void show() {
                if (isEmpty()) {
                    System.out.println("队列空");
                    return;
                }
                System.out.print("[");
                for (int i = front; i < front + size(); i++) {
                    System.out.print(arr[i % maxsize] + " ");
                }
                System.out.println("]");
            }

            // 头数据
            public int headData() {
                return arr[front];
            }

        }

 

LinkedBlockingQueue(两个独立锁提高并发)

基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者

端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE)。

基本属性:

    //队列大小,默认为Integer的最大值
    private final int capacity;

    //当前队列元素个数
    private final AtomicInteger count = new AtomicInteger();

    //队列的头部元素
    transient LinkedBlockingQueue.Node<E> head;

    //队列的尾部元素
    private transient LinkedBlockingQueue.Node<E> last;

    //出队锁
    private final ReentrantLock takeLock = new ReentrantLock();

    //出队的condition
    private final Condition notEmpty = takeLock.newCondition();

    //入队锁
    private final ReentrantLock putLock = new ReentrantLock();

    //入队的condition
    private final Condition notFull = putLock.newCondition();

LinkedBlockingQueue的属性和ArrayBlockingQueue的属性大致差不多,都是通过ReentrantLock和Condition来实现多线程之间的同步,而LinkedBlockingQueue却多了一个ReentrantLock,而不是入队和出队共用同一个锁。

那么为什么ArrayBlockingQueue只需要一个ReentrantLock而LinkedBlockingQueue需要两个ReentrantLock呢?

猜想:

首先,ReentrantLock肯定是越多越好,锁越多那么相同锁的竞争就越少;LinkedBlockingQueue分别有入队锁和出队锁,所以入队和出队的时候不会有竞争锁的关系;而ArrayBlockingQueue只有一个Lock,那么不管是入队还是出队,都需要竞争同一个锁,所以效率会低点。ArrayBlockingQueue是环形数组结构,入队的地址和出队的地址可能是同一个,比如数组table大小为1,那么第一次入队和出队需要操作的位置都是table[0]这个元素,所以入队和出队必须共用同一把锁;而LinkedBlockingQueue是链表形式,内存地址是散列的,入队的元素地址和出队的元素地址永远不可能会是同一个地址。所以可以采用两个锁,分别对入队进行加锁同步和对出队进行加锁同步即可。

 

PriorityBlockingQueue(compareTo 排序实现优先

是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。可以自定义实现

compareTo()方法来指定元素进行排序规则,或者初始化 PriorityBlockingQueue 时,指定构造

参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

DelayQueue(缓存失效、定时任务 )

是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现。队列中的元素必须实

现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才

能从队列中提取元素。我们可以将 DelayQueue 运用在以下应用场景:

 

1. 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询

DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。

 

2. 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从

DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的。

SynchronousQueue(不存储数据、可用于传递数据)

是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。

SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线

程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给

另 外 一 个 线 程 使 用 , SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和

ArrayBlockingQueue。

LinkedTransferQueue

是 一 个 由 链 表 结 构 组 成 的 无 界 阻 塞 TransferQueue 队 列 。 相 对 于 其 他 阻 塞 队 列 ,

LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。

 

1. transfer 方法:如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的

poll()方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如

果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素

被消费者消费了才返回。

 

2. tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费

者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否

接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。

对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传

入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时

还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。

LinkedBlockingDeque

是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。

双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其

他的阻塞队列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,

peekFirst,peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队

列的第一个元素。以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另

外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。但是 take 方法却等同

于 takeFirst,不知道是不是 Jdk 的 bug,使用时还是用带有 First 和 Last 后缀的方法更清楚。

在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在

“工作窃取”模式中

 

入队列 简单示意图:

入队列就是将入队节点添加到队列的尾部。

    第一步添加元素1。队列更新head节点的next节点为元素1节点。又因为tail节点默认情况下等于head节点,所以它们的next节点都指向元素1节点。 
    第二步添加元素2。队列首先设置元素1节点的next节点为元素2节点,然后更新tail节点指向元素2节点。 
    第三步添加元素3,设置tail节点的next节点为元素3节点。 
    第四步添加元素4,设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点。

源码简单分析:(基于链表数据结构,通过Note 节点实现)

将尾节点等其他的部分节点使用临时变量代替,一方面是为了去掉volatile变量的可变性,另一方面是为了减少volatile的性能影响

//以队列添加方法为例  (随便选的1个) 
public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        //入队前,创建一个入队节点 
        Node<E> n = new Node<E>(e, null);
        //死循环,入队不成功反复入队。 
        for (;;) {
            //t用来表示队列的尾节点,默认情况下等于tail节点
            Node<E> t = tail;
            //获得t节点的下一个节点。
            Node<E> s = t.getNext();
            //t节点默认情况下等于tail节点 所以会进if方法
            if (t == tail) {
                //如果尾节点没有下一个节点了,那么就用cas设置下一个节点是新入队的节点,上面创建那个
                if (s == null) {
                    if (t.casNext(s, n)) {
                        casTail(t, n);
                        return true;
                    }
                  //如果为节点有下一个节点,那么就用cas将下一个节点S设置为尾节点
                } else {
                    casTail(t, s);
                }
            }
        }
    }

 

 

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java 中的阻塞队列 的相关文章

随机推荐

  • ES的DSL语句

    1 相关概念 mysql与elasticsearch的概念对比 MySQL Elasticsearch 说明 Table Index 索引 index 就是文档的集合 类似数据库的表 table Row Document 文档 Docume
  • vue 代理服务,解决跨域问题

    1 解决跨域访问失败 在项目的根目录下创建一个 vue config js 文件 创建配置文件 module exports devServer proxy api target http cdn apc 360 cn ws true pa
  • java中^怎么用_^运算符在Java中做什么?

    插入符号 运算符在Java中具有什么功能 当我尝试这个 int a 5 n 它给我 for n 5 returns 0 for n 4 returns 1 for n 6 returns 3 所以我猜它不执行幂运算 但是那是什么呢 您可以张
  • 2023-9-12 完全背包问题

    题目链接 完全背包问题 初版 时间复杂度拉满 include
  • 第四章 随机变量的数据特征 4.3协方差及相关系数

    第四章 随机变量的数据特征 4 3协方差及相关系数 文章目录 第四章 随机变量的数据特征 4 3协方差及相关系数 协方差和相关系数的概念和性质 相关系数的意义 协方差和相关系数的概念和性质 其在一定程度上反应了X 与 Y之间的关系 按照定义
  • 如何更高效的提高CSDN浏览量 - 提升博客的曝光度

    前言 CSDN作为中国最大的IT技术社区 对于技术人员而言 拥有高浏览量的博客是提升个人知名度和影响力的关键 本文将介绍一个名为 CSDN Browsing Plus 的工具 通过它 我们可以更高级地增长CSDN的浏览量 提升博客的曝光度
  • 获取文件的绝对路径

    想要访问执行程序 exe 路径下的文件 有以下几个步骤 1 先通过函数GetModuleFileName获取执行程序的绝对路径 TCHAR szPath MAX PATH 0 GetModuleFileName NULL szPath MA
  • 4-3 嵌入法

    文章目录 4 3 嵌入法 基础知识 项目案例 动手练习 4 3 嵌入法 请参考 数据准备和特征工程 中的相关章节 调试如下代码 基础知识 import pandas as pd from sklearn model selection im
  • oracle 视图 其他用户,oracle创建视图中涉及到另外一个用户的表权限不足问题

    oracle创建视图中涉及到另外一个用户的表权限不足问题 在oracle中存储过程或者视图等对象创建时 如果涉及到另外一个用户的表 即使你已经grant dba了 也不行 必须显式地赋予查询权限 否则 你会发现在pl sql中可以执行语句
  • Linux mmap读/写触发共享文件页生命周期

    概述 Linux的mm内存子系统的核心功能就要要管理各种类型的page 确保能高效分配和释放 让物理内存得以最大化使用 初识内存系统往往关注的是page的申请和管理流程 容易忽略page的释放回收流程 其实理解mm中的内存回收和释放也是最核
  • 智能驾驶系统简介和测试要点分析

    智能驾驶系统是一种能够自主感知 决策和执行行驶任务的车辆控制系统 常见的智能驾驶系统包括 自动泊车系统 能够自动控制车辆完成泊车过程 包括寻找车位 转向 加速 制动等操作 自适应巡航系统 能够根据车速 车距和交通状况等因素自适应调整车速 并
  • react实现多选框切换样式逻辑

    import React Component from react class App extends Component constructor props super props this state checkboxItems con
  • time datetime模块 一篇精通

    time datetime python3中time模块的用法 导入 import time 查看time模块内置的能够使用的方法 print dir time 查看内置方法的说明 help time time method help ti
  • NVIDIA驱动安装

    需要去英伟达官网下载适合自己电脑的版本 nvidia网页可以自己测出你的电脑所需要的型号 首先Ctrl Alt F1进入字符界面 删除原有驱动版本 sudo apt get purge nvidia sudo apt get autorem
  • 执行.sh文件(shell脚本)的几种方式

    第一种 要进到shell脚本所在文件夹中 sh helloworld sh 第二种 要进到shell脚本所在文件夹中 bash helloworld sh 第三种 要进到shell脚本所在文件夹中 helloworld sh 第四种 hom
  • uniapp app以及小程序图片添加水印

    const ctx uni createCanvasContext myCanvas const ctx1 uni createCanvasContext myCanvas1 uni getImageInfo src this carInf
  • UiBot如何使用CSS Selector

    UiBot默认的数据抓取可以抓取整个表格 但是有时候我们并不想抓取整个表格 比方说 我们想将下图所有的头像复制到Excel里 这个时候我们无法使用数据抓取功能 因为我们并不是想抓取数据 而是要操作网页里的元素 将上图头像复制到Excel里的
  • Centos7 扩展系统磁盘大小

    Centos7扩展系统磁盘大小 系统盘大小不足 需要扩展系统盘大小 需要添加一块硬盘作为要使用的系统盘的扩展 我的是原来sda就有空间没有分配 所以不用单独再加磁盘了 直接使用sda的未分配的空间 如果是单独新增的一个磁盘例如 dev sd
  • Jenkins 联动 飞书 以签名校验方式 推送测试报告通知消息

    1 获取 飞书 Bot webhook 和 secret 2 python脚本 参考 Song Estelle 的文章 这里重写了部分代码 以签名校验方式发送通知 记得安装相关依赖 usr bin python3 encoding utf
  • Java 中的阻塞队列

    Java 中的阻塞队列 1 ArrayBlockingQueue 由数组结构组成的有界阻塞队列 2 LinkedBlockingQueue 由链表结构组成的有界阻塞队列 3 PriorityBlockingQueue 支持优先级排序的无界阻