阻塞队列(Blocking Queue)

一、队列的定义

说的阻塞队列,就先了解下什么是队列,队列也是一种特殊的线性表结构,在线性表的基础上加了一条限制:那就是一端入队列,一端出队列,且需要遵循FIFO(先进先出)的原则

队列的入口一端叫做队尾(rear),出口一端叫做队头(front),最简单的比如就是排队买火车票,新加入的排队者必须在队尾插入,而下一个排队结束的永远的队伍的第一个人

如下图示:

二、阻塞队列的定义

了解了队列,就很好理解什么是阻塞队列,既然是阻塞的,那就是需要等待,而队列的操作只有入队和出队,那就是在入队和出队的时候等待了,所以总结下阻塞队列就是:

1、入队时,如果队列已经满了,就阻塞等待直到队列中有位置可以插入

2、出队时,如果队列中为空,就阻塞等待直到队列中有数据可以出队列

三、Java队列的定义

Java集合体系分为Collection和Map两大阵营,而队列Queue则属于Collection系列

3.1、Queue接口

Queue接口继承之Collection接口,在Collection的继承上增加了队列独有的方法

boolean offer(E e):向队列中插入元素,插入成功返回true;插入失败返回false

E peek():获取队列头部的元素,如果队列为空则返回null

E poll():获取并移除头部的元素,队列为空则返回null,和peek的区别是获取头部元素之后将头部元素删除,符合队列的出队操作

E remove():获取并移除头部的元素,如果为空则抛异常

3.2、BlockingQueue接口

BlockingQueue接口是继承之Queue,在Queue方法的继承上添加了抛出异常操作,因为BlockingQueue上阻塞队列,所以就存在阻塞时间过长需要中断阻塞操作,或者是超时中断阻塞操作

而BlockingQueue也就是阻塞队列的顶级接口,BlockingQueue不同的实现类就是实现了不同的阻塞队列的效果

阻塞队列的入队和出队操作可以分为多种操作方式:

1、抛异常:入队时队列满了直接抛异常;出队时队列为空直接抛异常  如:add方法和remove方法

2、返回boolean:入队或出队成功返回true;失败返回false 如:offer方法和poll方法

3、阻塞:入队或出队失败则一直阻塞直到成功或者被其他线程唤醒 如:put方法和take方法

4、超时阻塞:入队和出队失败则阻塞指定的时间,超时了还是失败则取消阻塞 如:offer(timeout)和poll(timeout)方法

四、Java中阻塞队列的不同实现

4.1、ArrayBlockingQueue

基于数组实现的阻塞队列,初始化时需要定义数组的大小,也就是队列的大小,所以这个队列是一个有界队列

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,相当于数组是一个环形数组

环形数组的好处是增删数据时不需要挪动数组中的其他数据,只需要改变入队和出队的指针即可。而如果不是环形数组而是顺序数组的话,入队和出队就需要大量移动数据,否则数组空间一下就被用完了,性能较差

4.2、LinkedBlockingQueue

基于链表实现的阻塞队列,既然是链表,那么就可以看出这种阻塞队列含有链表的特性,那就是无界。但是实际上LinkedBlockingQueue是有界队列,默认大小是Integer的最大值,而也可以通过构造方法传入固定的capacity大小设置

LinkedBlockingQueue有一个内部类Node,属性有:E ite和Node next,所以可以看出LinkedBlockingQueue是一个单向链表

基本属性为:

 //队列大小,默认为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是链表形式,内存地址是散列的,入队的元素地址和出队的元素地址永远不可能会是同一个地址。所以可以采用两个锁,分别对入队进行加锁同步和对出队进行加锁同步即可。

4.3、DelayQueue

延迟队列,顾名思义就是只有当元素达到指定的时间后才可以从队列中取出。

根据这个思路可以满足下面几种需求:

1.定时任务:将任务放入队列中设置时间,循环阻塞地从队列中取任务,当从队列中取出数据就表示时间到了

2.缓存过期:循环从队列中取数据,一旦取出数据就表示数据过期了,直接删除即可

DelayQueue主要也是通过ReentrantLock+Condition来保证线程安全,而内部还采用了ProrityQueue来保证队列的优先级,实际就是按延时的时间来进行排序,延迟时间最短的排在队列的头部,

所以每次从头部获取的元素都是最先会过期的数据。

4.4、PriorityBlockingQueue

有优先级的阻塞队列,底层也是通过数组实现,默认初始容量为11,容量不够会自动扩容,扩容的最大值为Integer的最大值-8(有些虚拟机再实现数组头部存储内容所预留的空间),所以基本上可以认为是无界阻塞队列

扩容时的线程安全通过ReentrantLock+CAS+volatine实现

用法基本上和ArrayBlockingQueue差不多,只不过PriorityBlockingQueue相当于是无界,另外最重要的一点是它是有优先级的,既然有优先级就涉及到排序

PriorityBlockingQueue默认采用Comparator,或者存储的元素有自定义的比较器。

而存储数据的数组也不是简单的数组,而是采用了二叉堆的数据结构,同时满足完全二叉树+堆的数据结构(最大堆上层的元素必须大于所有下层的元素;最小堆或者上层的元素必须小于所有下层的元素)

而PriorityBlockingQueue默认是采用的最小堆,即每次取出的元素都是优先级最小的。那么问题来了,如果通过数组来实现一个二叉堆呢?见下面的图解:

先看下二叉堆的数据结构:

可以看出二叉堆的上层永远比下层的优先级要高,而且可以发现上层节点和子节点的关系:父节点序号=(子节点序号-1)/2;左字节点序号=父节点序号*2+1;右字节点序号=父节点序号*2+2

那么上面的数据结构就可以通过数组来存储,如下图示:

插入操作:将元素直接插入到最底层的节点,如上图插入节点88

1. 节点88成为节点40到左子节点,新加入的节点遍历和自己的父节点进行比较,

2.节点88比节点40大,两者互换位置;继续88比80大,互换位置,88比100小,位置不动,

3.结果是新插入的节点88成为节点100的左子节点,节点80成为节点88点右子节点,节点40成功节点80点右子节点

删除操作:阻塞队列的删除是只删除位置为0的元素,也就是节点100

1.直接取出数组[0]位置的元素

2.将队列尾部节点放到头部来,如上图就是把节点5放到最顶层,

3.然后将头节点依次和子节点进行比较,然后进行位置互换操作

所以每次插入和删除元素最多操作次数就是 二叉堆的高度

代码不再分析,主要就是实现来二叉堆的逻辑,并且通过ReentrantLock+Condition来保证线程间的同步效果

4.5、SynchronousQueue

SynchonousQueue是比较特殊的阻塞队列,特殊之处就是这个叫队列的队列没有容量,又或者说容量为0,所以一旦有元素插入此队列,由于没有容量,就必须被阻塞直到元素被取出

所以SynchronousQueue更像是一个通道,一端发数据,一端消费数据,数据不可以被堆积,发送方或消费方处理不过来或者是不处理都会导致阻塞

五、阻塞队列的应用

5.1、线程池

线程池的构造函数就包含了阻塞队列

Java集合--阻塞队列及各种实现的解析的更多相关文章

  1. Java多线程 阻塞队列和并发集合

    转载:大关的博客 Java多线程 阻塞队列和并发集合 本章主要探讨在多线程程序中与集合相关的内容.在多线程程序中,如果使用普通集合往往会造成数据错误,甚至造成程序崩溃.Java为多线程专门提供了特有的 ...

  2. Java:阻塞队列

    Java:阻塞队列 本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记 1. 概述 概念 队列 队列就可以想成是一个数组,从一头进入,一头出去,排队买饭 阻塞队列 B ...

  3. java 多线程阻塞队列 与 阻塞方法与和非阻塞方法

    Queue是什么 队列,是一种数据结构.除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的.无论使用哪种排序方式,队列的头都是调用remove()或poll()移 ...

  4. Java -- 使用阻塞队列(BlockingQueue)控制线程通信

    BlockingQueeu接口是Queue的子接口,但是它的主要作用并不是作为容器,而是作为线程同步的工具. 特征: 当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程 ...

  5. Java并发--阻塞队列

    在前面几篇文章中,我们讨论了同步容器(Hashtable.Vector),也讨论了并发容器(ConcurrentHashMap.CopyOnWriteArrayList),这些工具都为我们编写多线程程 ...

  6. JAVA可阻塞队列-ArrayBlockingQueue

    在前面的的文章,写了一个带有缓冲区的队列,是用JAVA的Lock下的Condition实现的,但是JAVA类中提供了这项功能,就是ArrayBlockingQueue, ArrayBlockingQu ...

  7. Java 集合与队列的插入、删除在并发下的性能比较

    这两天在写一个java多线程的爬虫,以广度优先爬取网页,设置两个缓存: 一个保存已经访问过的URL:vistedUrls 一个保存没有访问过的URL:unVistedUrls 需要爬取的数据量不大,对 ...

  8. java 可伸缩阻塞队列实现

    最近一年多写的最虐心的代码.必须好好复习java并发了.搞了一晚上终于测试都跑通过了,特此纪念,以资鼓励! import java.util.ArrayList; import java.util.L ...

  9. java多线程-阻塞队列BlockingQueue

    大纲 BlockingQueue接口 ArrayBlockingQueue 一.BlockingQueue接口 public interface BlockingQueue<E> exte ...

随机推荐

  1. Java语言和C++语言的差异

    Java采用了C及C++的语法格式,对于学习过C及C++的程序设计者来说,学习Java将有可能很轻松.但是,如果仔细检查Java语言的许多细节,就会发现Java取消了不少C及C++的特性,并且加入了一 ...

  2. Element UI表格组件技巧:如何简洁实现跨页勾选、跨页统计功能

    业务场景 在使用Element UI的Table组件时,常常面对这样的业务需求: 表格数据的每一项都要提供勾选框,当切换分页时,能够记忆所有页面勾选的数据,以实现批量提交不同页面勾选数据的功能.并且, ...

  3. 细数阿里云在使用 Docker 过程中踩过的那些坑

    昨天下午道哥在微信上丢给我一条新闻,看看,我们阿里云支持 Docker 企业版了.我打开一看,果然,阿里云发布了飞天敏捷版,开始支持企业级的 Docker 容器. 美国中部时间4月19日,阿里云在容器 ...

  4. 日日算法:Kruskal算法

    介绍 克鲁斯卡尔(Kruskal)算法是用来求出连通图中最小生成树的算法. 连通图:指无向图中任意两点都能相通的图. 最小生成树:指联通图的所有生成树中边权重的总和最小的树(即,找出一个树,让其联通所 ...

  5. SQL Server遍历表中记录的2种方法

    SQL Server遍历表一般都要用到游标,SQL Server中可以很容易的用游标实现循环,实现SQL Server遍历表中记录.本文将介绍利用使用表变量和游标实现数据库中表的遍历. 表变量来实现表 ...

  6. nat和静态映射

    拓扑图: 实验要求: 1.R2.R3能访问外网的4.4.4.4(4.4.4.4为R4上的环回接口,用来模拟inter网). 2.R4访问222.222.222.100其实访问到的是内网的192.168 ...

  7. 数学--数论--Miller_Rabin判断素数

    ACM常用模板合集 #include<iostream> #include<algorithm> #include<cstring> #include<cst ...

  8. CodeForces - 140A New Year Table (几何题)当时没想出来-----补题

    A. New Year Table time limit per test2 seconds memory limit per test256 megabytes inputstandard inpu ...

  9. 使用 kind 快速搭建 kubernetes 环境

    使用 kind 快速搭建 Kubernetes 环境 Intro kind(Kubernetes IN Docker) 是一个基于 docker 构建 Kubernetes 集群的工具,非常适合用来在 ...

  10. Composition API

    介绍 Composition API的主要思想是,我们将它们定义为从新的 setup 函数返回的JavaScript变量,而不是将组件的功能(例如state.method.computed等)定义为对 ...