用Java如何设计一个阻塞队列,然后说说ArrayBlockingQueue和LinkedBlockingQueue
前言
用Java如何设计一个阻塞队列,这个问题是在面滴滴的时候被问到的。当时确实没回答好,只是说了用个List,然后消费者再用个死循环一直去监控list的是否有值,有值的话就处理List里面的内容。回头想想,自己真是一个大傻X,也只有我才会这么设计一个阻塞队列(再说,我这也不是阻塞的队列)。
结果自己面试完之后,也没去总结这部分知识,然后过了一段时间,某教育机构的面试又被问到类似的问题了,只不过是换了一个形式,“请用wait方法和notify方法实现一套有生产者和消费者的这种逻辑”。然后我就又蒙圈了,追悔莫及,为啥我没有去了解一下这部分知识,所以这次我准备好好总结一下这部分内容。
具体实现
如果说实现一个队列,那么一个LinkedList的这种实现了Queue接口的都可以直接使用,或者自己写一个先进先出的Array都可以。
但是要做到阻塞就还需要进行阻塞的实现,就是说当队列是空时,如果再继续从队列中获取数据,将会被阻塞,直到有新的数据入队列才停止阻塞;还有当队列已经满了(到达设置的最大容量),再往队列里添加元素的操作也会被阻塞,直到有数据从队列中被移除。
这里首先要有一个锁,保证同时只能有一个线程执行出队列、同时只能有一个线程执行入队列。而执行出队列和入队列的线程的阻塞和唤醒,是靠wait()方法和notifyAll()方法来实现的。
代码实现如下:
public class MyBlockQueue {
/**
* 队列长度默认为10
*/
private int limit = 10;
private Queue queue = new LinkedList<>();
/**
* 初始化队列容量
* @param limit 队列容量
*/
public MyBlockQueue(int limit){
this.limit = limit;
}
/**
* 入队列
* @param object 队列元素
* @throws InterruptedException
*/
public synchronized boolean push(Object object) throws InterruptedException{
// 如果队列已满,再来添加队列的线程就直接阻塞等待。
while (this.queue.size() == this.limit){
wait();
}
// 如果队列为空了,就唤醒所有阻塞的线程。
if(this.queue.size() == 0){
notifyAll();
}
// 入队
boolean add = this.queue.offer(object);
return add;
}
/**
* 出队列
* @return
* @throws InterruptedException
*/
public synchronized Object pop() throws InterruptedException{
// 如果出队列时,队列为空,则阻塞队列。
while (this.queue.size() == 0){
wait();
}
// 如果队列重新满了之后,唤醒阻塞的所有线程。
if(this.queue.size() == this.limit){
notifyAll();
}
Object poll = this.queue.poll();
return poll;
}
}
Java中阻塞队列的实现
首先我们先来归纳一下,Java中有哪些已经实现好了的阻塞队列:
队列 | 描述 |
---|---|
ArrayBlockingQueue |
基于数组结构实现的一个有界阻塞队列 |
LinkedBlockingQueue |
基于链表结构实现的一个有界阻塞队列 |
PriorityBlockingQueue |
支持按优先级排序的无界阻塞队列 |
DelayQueue |
基于优先级队列(PriorityBlockingQueue)实现的无界阻塞队列 |
SynchronousQueue |
不存储元素的阻塞队列 |
LinkedTransferQueue |
基于链表结构实现的一个无界阻塞队列 |
LinkedBlockingDeque |
基于链表结构实现的一个双端阻塞队列 |
我们这次主要来看一下ArrayBlockingQueue
和LinkedBlockingQueue
这两个阻塞队列。
在介绍这两个阻塞队列时,先普及两个知识,就是ReentrantLock
和Condition
的几个方法。因为JDK中的这些阻塞队列加锁时基本上都是通过这两种方式的API来实现的。
ReentrantLock
- lock():加锁操作,如果此时有竞争会进入等待队列中阻塞直到获取锁。
- lockInterruptibly():加锁操作,但是优先支持响应中断。
- tryLock():尝试获取锁,不等待,获取成功返回true,获取不成功直接返回false。
- tryLock(long timeout, TimeUnit unit):尝试获取锁,在指定的时间内获取成功返回true,获取失败返回false。
- unlock():释放锁。
Condition
通常和ReentrantLock一起使用的
- await():阻塞当前线程,并释放锁。
- signal():唤醒一个等待时间最长的线程。
ArrayBlockingQueue
构造方法
首先来看一下ArrayBlockingQueue
的初始化方法
ArrayBlockingQueue
是有三个构造方法的,但是都是基于ArrayBlockingQueue
(int capacity, boolean fair)来实现的,所以只要了解这一个构造方法即可。
主要是:
- 采用数组结构来初始化队列,并定义队列长度;
- 然后创建全局锁,出队和入队时都要先获取锁再执行操作;
- 创建阻塞线程的非空等待队列;
- 创建阻塞线程的非满等待队列;
入队列
下面来看一下入队列操作
无论put()方法还是offer()方法,在入队列时都是先加锁,然后最终入队列都是调用的enqueue()方法,只不过put方法是阻塞入队列,就是说如果队列已满,入队列的线程会被阻塞,而offer方法则不会阻塞入队列不成功的线程,offer执行入队列不成功的线程直接返回失败,其实还有一个add方法也是入队列,和offer方法一直都是非阻塞入队。
下面来一下enqueue()方法。
enqueue()方法其实步骤也不复杂,主要是入队列操作是从数组的尾部入,然后出队列是从队列的头部出,这样当队列满了的时候,下一次再入队列时的位置应该从队列的头部开始入了。所以才会有重置putIndex的操作。
如果不能理解可以看下面的图片,正常队列未满时,从数组尾部入队列,头部出队列。
当队列满了之后,入队列就要从数组头部位置开始了。
出队列
下面来看一下ArrayBlockingQueue
的出队列方法
我们通过上面两张源码的截图可以看出来,无论是poll()方法还是take()方法,最终出队列调用的都是dequeue()方法,只不过take()是阻塞的方式出队列,当队列为空时直接将出队列线程阻塞并放到等待队列中。
那么dequeue()是如何出队列的呢?
我们通过源码可知,出队列是根据出队列索引takeIndex来决定该出哪一个元素了,如果当前出队列的元素的索引正好是数组容量的最后一个元素,那么出队列索引takeIndex也要重新从头开始记录了。后面再更新迭代器中的数据,以及唤醒阻塞中的入队线程。
还有两个出队列的方法remove(Object o)
和removeAt(final int removeIndex)
这两个方法稍微复杂一些,因为首先要定位到要移除的元素的位置,然后再执行出队操作,remove最终执行的出队方法是依赖removeAt(final int removeIndex)
,而removeAt
的出队操作是定位到要移除的元素位置后,将takeIndex位置的元素替换掉要移除的元素,就完成了出队操作 。
LinkedBlockingQueue
构造方法
LinkedBlockingQueue
的初始化队列的数据信息时是在构造方法中进行的,但是实现阻塞队列需要的核心能力是在JVM为对象分配空间时就初始化好了的。
入队列
从初始化数据的时候可以看到,LinkedBlockingQueue
是有两个锁的,入队列有入队列的锁,出队列有出队列的锁,是两个独立的重入锁。这样入队列和出队列相互对立的处理,大大的提高了队列的吞吐量。
我们看到LinkedBlockingQueue
的入队列的两个方法put和offer(其实还有一个add方法,但是具体实现也是调用的offer方法),put方法是阻塞入队,即当队列满了的时候阻塞入队列的线程,而offer则不是阻塞入队,入队列成功即返回true否则返回false。
这两个方法底层调用的都是enqueue()方法,我们看一下这个方法具体是怎么执行的入队列。
enqueue()
方法逻辑比较简单,就是将元素添加到链表的尾部。
出队列
LinkedBlockingQueue
的出队列方法,是先获取出队列的takeLock
,然后再执行出队列方法。
take方法和poll方法前者在队列为空后,会阻塞出队列的线程,后者poll方法则不会在队列为空时阻塞出队列线程,会直接返回null。
无论是take方法还是poll方法都是调用的dequeue()
方法执行的出队列,那么看一下dequeue()
方法的实现吧。一直忘记说了,我这次贴出来的源码都是JDK1.8版本的。
我们看到dequeue()
执行了一个比较绕的逻辑,主要意思是将头节点后的第一个不为null的节点移除队列了,并设置了新的头节点位置。
我们来仔细拆分一下步骤,就好理解了,初始时,头节点的值是null(new Node(null)
)但是next指向的是队列中的第二个节点。
- 第一步把head节点会把自己的next节点从指向第二节点,改成指向自己,这样,本来head节点的值就是null,然后现在next也是一个空节点了,这样的节点GC的时候就会被优先回收掉了。
- 第二步把原先head节点的下一个节点的值赋值给head,这样原先的第二节点就成为了head节点,然后将新head节点的数据返回。
- 将新head节点的值设置为null,这样就新的节点的也就和原先的head节点的数据形式一样了。
我们可以通过下面图来更清晰的看一下:
我们再来看一下出队列的另一个方法remove。
执行remove()
方法的时候,要将出队列锁和入队列的锁都加上,这两个操作要等待remove()
方法执行完毕后再操作。为了就是保证在remove()
方法寻找指定元素时有入队和出队操作导致遍历操作混乱。
我们再来看一下unlink()
方法,主要还是将元素从链表中移除,若移除的元素为last元素,做一些处理等。
总结
- 自己实现了阻塞队列,首先要有锁来保证入队列和出队列的线程在队列满和队列为空时阻塞主入队列线程和出队列线程。然后再队列有空间后唤醒入队列线程,在队列有数据时唤醒出队列线程。
ArrayBlockingQueue
和LinkedBlockingQueue
都是有界的阻塞队列(LinkedBlockingQueue
的默认长度为Int的最大值也暂且归为是有界),ArrayBlockingQueue
是通过数据来实现阻塞队列的,并且是依赖ReentrantLock
和Condition
来进行加锁的。LinkedBlockingQueue
是通过链表来实现阻塞队列的,也是依赖ReentrantLock
和Condition
来完成加锁的。ArrayBlockingQueue
采用的全局唯一锁,入队列和出队列只能有一个操作同时进行,LinkedBlockingQueue
入队列和出队列分别采用对立的重入锁,入队列和出队列可分开执行,所以吞吐量比ArrayBlockingQueue
更高。ArrayBlockingQueue
采用数组来实现队列,执行过程中并不会释放内存空间,所以需要更多的连续内存;LinkedBlockingQueue
虽然不需要大量的联系内存,但是在并发情况下,会创建和置空大量的对象,很依赖GC的处理效率。
用Java如何设计一个阻塞队列,然后说说ArrayBlockingQueue和LinkedBlockingQueue的更多相关文章
- JAVA中常见的阻塞队列详解
在之前的线程池的介绍中我们看到了很多阻塞队列,这篇文章我们主要来说说阻塞队列的事. 阻塞队列也就是 BlockingQueue ,这个类是一个接 口,同时继承了 Queue 接口,这两个接口都是在JD ...
- Java并发编程之阻塞队列
1.什么是阻塞队列? 队列是一种数据结构,它有两个基本操作:在队列尾部加入一个元素,从队列头部移除一个元素.阻塞队里与普通的队列的区别在于,普通队列不会对当前线程产生阻塞,在面对类似消费者-生产者模型 ...
- java并发编程:阻塞队列
一.几种主要的阻塞队列 自从Java 1.5之后,在java.util.concurrent包下提供了若干个阻塞队列,主要有以下几个: ArrayBlockingQueue:基于数组实现的一个阻塞队列 ...
- Java并发编程:阻塞队列(转载)
Java并发编程:阻塞队列 在前面几篇文章中,我们讨论了同步容器(Hashtable.Vector),也讨论了并发容器(ConcurrentHashMap.CopyOnWriteArrayList), ...
- Java多线程-新特征-阻塞队列ArrayBlockingQueue
阻塞队列是Java5线程新特征中的内容,Java定义了阻塞队列的接口java.util.concurrent.BlockingQueue,阻塞队列的概念是,一个指定长度的队列,如果队列满了,添加新元素 ...
- 【转】Java并发编程:阻塞队列
在前面几篇文章中,我们讨论了同步容器(Hashtable.Vector),也讨论了并发容器(ConcurrentHashMap.CopyOnWriteArrayList),这些工具都为我们编写多线程程 ...
- Java并发编程:阻塞队列 <转>
在前面几篇文章中,我们讨论了同步容器(Hashtable.Vector),也讨论了并发容器(ConcurrentHashMap.CopyOnWriteArrayList),这些工具都为我们编写多线程程 ...
- 利用ReentrantLock简单实现一个阻塞队列
借助juc里的ReentrantLock实现一个阻塞队列结构: package demo.concurrent.lock.queue; import java.util.concurrent.lock ...
- 12、Java并发编程:阻塞队列
Java并发编程:阻塞队列 在前面几篇文章中,我们讨论了同步容器(Hashtable.Vector),也讨论了并发容器(ConcurrentHashMap.CopyOnWriteArrayList), ...
随机推荐
- Linux提权
讲Linux提权之前,我们先看看与Linux有关的一些知识: 我们常说的Linux系统,指的是Linux内核与各种常用软件的集合产品,全球大约有数百款的Linux系统版本,每个系统版本都有自己的特性和 ...
- 【编译原理】求First和Follow
写这篇博客的原因,是因为考试前以为自己已经将这个问题弄清楚了,但是,考试的时候,发现自己还是不会,特别是求follow集合.虽然考试结束了,希望屏幕前的你,可以真正理解这个问题. 码字和做视频都不容易 ...
- Java虚拟机栈和PC寄存器
PC Register介绍 JVM中的程序计数寄存器(Program Counter Register)中,Register 的命名源于CPU的寄存器,寄存器存储指令相关的现场信息.CPU只有把数据装 ...
- Java匿名对象导致的内存泄漏
这几天与在某群与群友讨论了Runnable匿名对象导致内存泄漏的相关问题,特此记录一下. 示例代码如下: package com.memleak.memleakdemo; public class L ...
- Spring Boot 2.5.0 发布:支持Java16、Gradle 7、Datasource初始化机制调整
今年520的事情是真的多,娱乐圈的我们不管,就跟DD一起来看看 Spring Boot 2.5.0 的发布吧!看看都带来了哪些振奋人心的新特性和改动! 主要更新 支持 Java 16 支持 Gradl ...
- [Qt] 信号和槽
信号与槽:是一种对象间的通信机制 观察者模式:当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal).这种发出是没有目的的,类似广播.如果有对象对这个信号感兴趣,它就 ...
- [Python] 条件 & 循环
条件语句 不加 () 结尾加 : elif else 和 if 成对使用 省略判断条件 String:空字符串为False,其余为True int:0为False,其余为True Bool:True为 ...
- SecureCRT自动保存日志设置
SecureCRT自动保存日志设置原创杭州_燕十三 最后发布于2017-03-26 22:00:08 阅读数 24731 收藏展开 嵌入式开发经常由于无法debug而只能使用串口打印日志的方式调试代码 ...
- Linux_配置辅助DNS服务(基础)
[RHEL8]-DNSserver1:[RHEL7]-DNSserver2:[Centos7]-DNSclient !!!测试环境我们首关闭防火墙和selinux(DNSserver1.DNSserv ...
- 配置yum仓库的三种方法光盘镜像、nginx、sftp
方法一: 1.安装ftp服务 [root@oldboy ~]# yum -y install vsftpd 2.查看vsftpd相关的配置文件和目录 rpm -ql vsftpd # 查看vsftpd ...