阻塞队列 BlockingQueue

阻塞队列概述

阻塞队列(Blocking Queue)是 Java 并发编程中的重要工具,它是一个特殊的队列,具有阻塞操作的特性。在阻塞队列中,当队列为空时,从队列中获取元素的操作会被阻塞,直到队列中有可用元素;当队列已满时,往队列中添加元素的操作也会被阻塞,直到队列有空间可用。
Java 并发包中提供了多种类型的阻塞队列,常用的包括:

  1. ArrayBlockingQueue:基于数组实现的有界阻塞队列。
  2. LinkedBlockingQueue:基于链表实现的有界或无界阻塞队列。
  3. PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列。
  4. SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待对应的移除操作。
    阻塞队列在并发编程中起着非常重要的作用,它能够帮助我们实现生产者-消费者模式,并且在多线程环境下提供了一种线程安全的队列操作方式。通过阻塞队列,我们能够避免自己手动编写复杂的线程同步机制,简化并发编程的复杂性。
    使用阻塞队列可以帮助我们有效地解决多线程场景下的数据共享和线程同步问题,使得程序设计更加简洁和安全。因此,在并发编程中,阻塞队列是一个非常重要且实用的工具。

阻塞队列详解

JDK7 提供了 7 个阻塞队列。分别是
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
Java5之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好,wait ,notify,notifyAll,sychronized 这些关键字。而在 java5之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。 BlockingQueue 接口是 Queue 的子接口,它的主要用途并不是作为容器,而是 作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向 BlockingQueue 放入元素时,如果队列已满,则线程被阻塞当消费者线程试 图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具 有这个特性,所以在程序中多个线程交替向 BlockingQueue 中放入元素,取出 元素,它可以很好的控制线程之间的通信。 阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据 的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。

BlockingQueue 是个接口,你需要使用它的实现之一来使用 BlockingQueue,Java.util.concurrent 包下具有以下 BlockingQueue 接口的实现类:
ArrayBlockingQueue
是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)
DelayQueue(缓存失效、定时任务 )
是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现。队列中的元素必须实 现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才 能从队列中提取元素。我们可以将 DelayQueue 运用在以下应用场景:
1缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。
2定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从 DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的。
DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现
java.util.concurrent.Delayed 接口。

LinkedBlockingQueue
(两个独立锁提高并发)
基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE)

带时间的 Offer 操作-生产者
在 ArrayBlockingQueue 中已经简单介绍了 Offer()方法,LinkedBlocking 的 Offer 方法类似,在此就不过多去
介绍。这次我们从介绍下带时间的 Offer 方法
带时间的 poll 操作-消费者
获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回 null。
✓ put 操作-生产者
与带超时时间的 poll 类似不同在于 put 时候如果当前队列满了它会一直等待其他线程调用 notFull.signal 才会被唤醒。
✓ take 操作-消费者
与带超时时间的 poll 类似不同在于 take 时候如果当前队列空了它会一直等待其他线程调用 notEmpty.signal()才会被唤醒。
✓ size 操作-消费者
当前队列元素个数,如代码直接使用原子变量 count 获取。
✓ peek 操作
获取但是不移除当前队列的头元素,没有则返回 null。
✓ remove 操作
删除队列里面的一个元素,有则删除返回 true,没有则返回 false,在删除操作时候由于要遍历队列所以加了双重锁,也就是在删除过程中不允许入队也不允许出队操作。

LinkedBlockingQueue 安全分析总结
仔细思考下阻塞队列是如何实现并发安全的维护队列链表的,先分析下简单的情况就是当队列里面有多个元素时
候,由于同时只有一个线程(通过独占锁 putLock 实现)入队元素并且是操作 last 节点(,而同时只有一个出队线程
(通过独占锁 takeLock 实现)操作 head 节点,所以不存在并发安全问题。
LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如
果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限

PriorityBlockingQueue(compareTo 排序实现优先)

是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。可以自定义实现
compareTo()方法来指定元素进行排序规则,或者初始化 PriorityBlockingQueue 时,指定构造
参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
PriorityBlockingQueue 是 一 个 无 界 的 并 发 队 列 。 它 使 用 了 和 类
java.util.PriorityQueue 一 样 的 排 序 规 则 。 你 无 法 向 这 个 队 列 中 插 入 null 值 。 所 有 插 入 到
PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于
你自己的 Comparable 实现。

SynchronousQueue(不存储数据、可用于传递数据)
是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。
SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线
程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给
另 外 一 个 线 程 使 用 , SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和
ArrayBlockingQueue。
SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。
如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队
列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插
入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。

常用的并发队列有阻塞队列和非阻塞队列,前者使用锁实现,后者则使用 CAS 非阻塞算法实现。
PS:至于非阻塞队列是靠 CAS 非阻塞算法,在这里不再介绍,大家只用知道,Java 非阻塞队列是使用 CAS 算法下面我们先介绍阻塞队列。
阻塞队列 (BlockingQueue)是 Java util.concurrent 包下重要的数据结构,BlockingQueue 提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于 BlockingQueue 实现的。

一个线程往里边放,另外一个线程从里边取的一个 BlockingQueue。一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列

阻塞队列有界和无界

阻塞队列,是一种特殊的队列,它在普通队列的基础上提供了两个附加功能当队列为空的时候,获取队列中元素的消费者线程会被阻塞,同时唤醒生产者线程。当队列满了的时候,向队列中添加元素的生产者线程被阻塞,同时唤醒消费者线程。
其中,阻塞队列中能够容纳的元素个数,通常情况下是有界的,比如我们实例化个 ArrayBlockingList,可以在构造方法中传入一个整形的数字,表示这个基于数组的阻塞队列中能够容纳的元素个数。这种就是有界队列。
而无界队列,就是没有设置固定大小的队列,不过它并不是像我们理解的那种元素没有任何限制,而是它的元素存储量很大,像 LinkedBlockingQueue,它的默认队列长度是 Integer.Max_Value,所以我们感知不到它的长度限制。无界队列存在比较大的潜在风险,如果在并发量较大的情况下,线程池中可以几乎无限制的添加任务,容易导致内存溢出的问题

阻塞队列在生产者消费者模型的场景中使用频率比较高,比较典型的就是在线程池中,通过阻塞队列
来实现线程任务的生产和消费功能。基于阻塞队列实现的生产者消费者模型比较适合用在异步化性能提升的场景,以及做并发流量缓冲类的场景中!在很多开源中间件中都可以看到这种模型的使用,比如在Zookeeper源码中就大量用到了阻塞队列实现的生产者消费者模型。

ArrayBlockingQueue阻塞队列

基于数组的阻塞队列 ArrayBlockingQueue 原理
阻塞队列(BlockingQueue)是在队列的基础上增加了两个附加操作,在队列为空的时候,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
由于阻塞队列的特性,可以非常容易实现生产者消费者模型,也就是生产者只需要关心数据的生产,消费者只需要关注数据的消费,所以如果队列满了,生产者就等待,同样,队列空了,消费者也需要等待。
要实现这样的一个阻塞队列,需要用到两个关键的技术,队列元素的存储、以及线程阻塞和唤醒。
而 ArrayBlockingQueue 是基于数组结构的阻塞队列,也就是队列元素是存储在一个数组结构里面,并且由于数组有长度限制,为了达到循环生产和循环消费的
目的,ArrayBlockingQueue 用到了循环数组。而线程的阻塞和唤醒,用到了 J.U.C 包里面的 ReentrantLock 和 Condition。Condition 相当于 wait/notify 在 JUC 包里面的实现。
offer 方法 在队尾插入元素,如果队列满则返回 false,否者入队返回 true。

public boolean offer(E e) {
    //e 为 null,则抛出 NullPointerException 异常
    checkNotNull(e);
//获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
    //如果队列满则返回 false
    if (count == items.length)
        return false;
    else {
        //否者插入元素
        insert(e);
        return true;
    }
} finally {
    //释放锁
    lock.unlock();
}
}
private void insert(E x) {
    //元素入队
    items[putIndex] = x;
    //计算下一个元素应该存放的下标
    putIndex = inc(putIndex);
    ++count;
    notEmpty.signal();
}
//循环队列,计算下标
final int inc(int i) {
    return (++i == items.length) ? 0 : i;
}

这里由于在操作共享变量前加了锁,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的,
而不是在 CPU 缓存或者寄存器里面的值,释放锁后修改的共享变量值会刷新会主内存中。
另外这个队列是使用循环数组实现,所以计算下一个元素存放下标时候有些特殊。另外 insert 后调用
notEmpty.signal();是为了激活调用 notEmpty.await()阻塞后放入 notEmpty 条件队列中的线程

其他方法
offer 方法 在队尾插入元素,如果队列满则返回 false,否者入队返回 true
Put 操作 在队列尾部添加元素,如果队列满则等待队列有空位置插入后返回

Poll 操作 从队头获取并移除元素,队列为空,则返回 null。
Take 操作 从队头获取元素,如果队列为空则阻塞直到队列有元素。
Peek 操作 返回队列头元素但不移除该元素,队列为空,返回 null
Size 操作 获取队列元素个数,非常精确因为计算 size 时候加了独占锁,其他线程不能入队或者出队或者删除元素
总结
ArrayBlockingQueue 通过使用全局独占锁实现同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较
大,有点类似在方法上添加 synchronized 的意味。其中 offer,poll 操作通过简单的加锁进行入队出队操作,而 put,take则使用了条件变量实现如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。另外相比 LinkedBlockingQueue,ArrayBlockingQueue 的 size 操作的结果是精确的,因为计算前加了全局锁。

ArrayBlockingQueue(公平、非公平)
用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下
不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当
队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入
元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐
量。我们可以使用以下代码创建一个公平的阻塞队列:
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
在这里插入图片描述
在这里插入图片描述

如上图 ArrayBlockingQueue 内部有个数组 items 用来存放队列元素,putindex 下标标示入队元素下标,
takeIndex 是出队下标,count 统计队列元素个数,从定义可知道并没有使用 volatile 修饰,这是因为访问这些变量使用都是在锁块内,并不存在可见性问题。另外有个独占锁 lock 用来对出入队操作加锁,这导致同时只有一个线程可以访问入队出队,另外 notEmpty,notFull 条件变量用来进行出入队的同步。
另外构造函数必须传入队列大小参数,所以为有界队列,默认是 Lock 为非公平锁。

public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull = lock.newCondition();
}

在多线程操作下,一个数组中最多只能存入 3 个元素。多放入不可以存入数组,或等待某线程对数组中某* 个元素取走才能放入,要求使用 java 的多线程来实现。
package com.lc.other.thread.blockingQueue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
* @Author lc
* @description:在多线程操作下,一个数组中最多只能存入 3 个元素。多放入不可以存入数组,或等待某线程对数组中某
* 个元素取走才能放入,要求使用 java 的多线程来实现。(面试)
* @Date 2023/3/8 16:09
*/
public class ArrayBlockingQueueTest {
    public static void main(String[] args) {
        final BlockingQueue queue = new ArrayBlockingQueue(3);
        for (int i = 0; i < 2; i++) {
            new Thread() {
                public void run() {
                    while (true) {
                        try {
                            Thread.sleep((long) (Math.random() * 1000));
                            System.out.println(Thread.currentThread().getName() + "准备放数据!");
                            queue.put(1);
                            System.out.println(Thread.currentThread().getName() + "已经放了数据," + "队列目前有" + queue.size() + "个数据");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
        }

        new Thread() {
            public void run() {
                while (true) {
                    try {
                        //将此处的睡眠时间分别改为 100 和 1000,观察运行结果
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + "准备取数据!");
                        System.err.println(queue.take());
                        System.out.println(Thread.currentThread().getName() + "已经取走数据," +
                                           " 队列目前有" + queue.size() + "个数据");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }.start();
    }
}

BlockingQueue 的方法
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到
立即执行的话,每个方法的表现也不同。这些方法如下:
阻塞队列提供了四种处理方法:
在这里插入图片描述
四组不同的行为方式解释:
抛异常:如果试图的操作无法立即执行,抛一个异常。
特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
无法向一个 BlockingQueue 中插入 null。如果你试图插入 null,BlockingQueue 将会抛出一个
NullPointerException.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

思静鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值