JDK容器学习之Queue: ArrayBlockingQueue
时间:2022-04-27
本文章向大家介绍JDK容器学习之Queue: ArrayBlockingQueue,主要内容包括基于数组阻塞队列 ArrayBlockingQueue、II. 阻塞实现原理、III. 应用场景及case、IV. 小结、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。
基于数组阻塞队列 ArrayBlockingQueue
前面学习了基于数组的非阻塞双端队列
ArrayDeque
,其内部维护一个数组和指向队列头和队列尾索引的两个成员变量;本篇则探究下基于数组的阻塞队列是什么样的数据结构,又有什么特性,相较于ArrayDeque
又有什么异同;然后就是使用场景了
I. 底层数据结构
先看内部成员变量定义, 和 ArrayDequeue
相比,差别不大,一个数组,两个索引;此外多了一个锁和两个判定条件
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
注意
- 底层数据结构依然是数组,注释上并没有说明要求容量是2的n次方(ArrayDequeue有这个强制限定)
- 初始化时必须指定队列的容量(即数组的长度,构造方法中的必选参数)
-
count
直接表示队列的元素个数(注意DelayQueue是通过遍历来获取队列长度,且并发修改会有问题,那么这个是如何保证并发的?) - 留一个疑问,塞入队列超过数组容量,是否会出现扩容
数据结构如下图
![aryBlockQueueStruct.jpeg](quiver-image-url/5AEE167065A2E49EB3DDBC449BCF991E.jpg =374x165)
II. 阻塞实现原理
0. Prefer
分析阻塞原理之前,先通过注释解释下ArrayBlockingQueue
的使用场景
- 先进先出队列(队列头的是最先进队的元素;队列尾的是最后进队的元素)
- 有界队列(即初始化时指定的容量,就是队列最大的容量,不会出现扩容,容量满,则阻塞进队操作;容量空,则阻塞出队操作)
- 队列不支持空元素
1. 进队
通用的进队方法如下,是非阻塞的方式,当数组满时,直接返回false,为保证并发安全,进队操作是加锁实现
public boolean offer(E e) {
// 非空校验
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock(); // 进队加锁
try {
if (count == items.length)
// 队列满,则直接返回false
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
// 直接将元素塞入数组
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
阻塞方式的进队实现如下
public void put(E e) throws InterruptedException {
checkNotNull(e); // 非空判断
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 获取锁
try {
while (count == items.length) {
// 一直阻塞,知道队列非满时,被唤醒
notFull.await();
}
enqueue(e); // 进队
} finally {
lock.unlock();
}
}
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
// 阻塞,知道队列不满
// 或者超时时间已过,返回false
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
源码分析,阻塞入队的逻辑比较清晰,小结一下
- 非阻塞调用方式
offer(e)
- 阻塞调用方式
put(e)
或offer(e, timeout, unit)
- 阻塞调用时,唤醒条件为超时或者队列非满(因此,要求在出队时,要发起一个唤醒操作)
- 进队成功之后,执行
notEmpty.signal()
唤起被阻塞的出队线程
2. 出队
非阻塞出队方法如下
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
// 直接将队头扔出去,并置空数组中该位置
// 并移动队列头到下一位
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
// 保障在遍历时,可以进行出队操作
itrs.elementDequeued();
notFull.signal();
return x;
}
阻塞的实现,逻辑比较清晰,首先竞争锁,判断是否为空,是阻塞直到非空;否则弹出队列头元素
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
public E poll(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
小结
- 非阻塞出队调用
poll()
方法 - 阻塞出队调用
take()
或poll(long,TimeUnit)
方法 - 出队之后,会唤醒因队列满被阻塞的入队线程
- 出队count计数减1(因为每次只能一个线程出队,所以count的值可以保证并发准确性)
- 支持在遍历队列时,出队
III. 应用场景及case
创建线程池时,通常会用到 ArrayBlockingQueue
或者LinkedBlockingQueue
,如
new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(2));
延迟队列也是并发安全,ArrayBlockingQueue
相比较 DelayQueue
应用场景的区别主要在
- 有界和无界(ArrayBlockingQueue不会扩容,而DelayQueue会出现扩容)
- 前者队列非空就可以出队;后者则需要队列头生效(
getDelay()
返回值小于0) - 前者队列满,则无法入队;后者一直都可以入队
- 前者FIFO;后者优先级队列,延迟时间小的优先出队
IV. 小结
基于数组阻塞队列ArrayBlockingQueue
- 有界数组阻塞队列,FIFO,先进先出,初始化时指定数组长度
- 阻塞出队方法:
take()
或poll(long, TimeUnit)
- 非阻塞出队方法:
poll()
- 阻塞入队方法:
offer(E, long, TimeUnit)
或put(E)
- 非阻塞入队方法:
offer(E)
add(E)
- 队列为空,出队会被阻塞;队列满时,进队会被阻塞
- 根据内部计数count,可以直接获取队列长度(count的并发安全是由进队出队上锁,保证同一时刻只有一个线程修改count值保证的)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Oracle 数据库直接执行本地sql文件、sql脚本实例演示
- Oracle 数据库利用回收站恢复删除的表实例演示
- Linux 命令利用scp实现从服务器共享地址上传下载文件、文件夹实例演示,scp命令的参数详解
- Oracle 数据库利用sql语句判断某个表是否是临时表实例演示,达梦数据库查询出所有临时表
- JavaScript 技术篇-一段js代码展示可以随鼠标移动变换样式的卡通人物,动态女生眼睛跟着鼠转动
- PyQt5 图形界面-用Qt Designer来设计UI界面,并转化为python代码运行
- Python 技术篇-python生成html源码功能实现演示,html代码自动生成技巧。列表生成式的灵活应用。
- Python 技术篇-pyHook键盘鼠标监听事件,监测鼠标键盘按键。超简单,几行代码搞定。
- Python 技术篇-用mutagen库提取MP3歌曲图片
- Python 典藏篇-Microsoft Visual C++ 14.0 is required,官方vc++运行库工具一键式解决!
- Python 技术篇-邮件写入html代码,邮件发送表格,邮件发送超链接,邮件发送网络图片
- 面经手册 · 第11篇《StringBuilder 比 String 快?空嘴白牙的,证据呢!》
- domReady的理解
- Map集合排序
- Chrome 技术篇-一台电脑设置多个独立chrome方法。chrome独立多开技术。