文章目录
BlockingQueue
BlockingQueue
接口定义了很多插入和删除的方法,这里总结梳理如下:
添加
-
容器满了,抛异常
add(e) -
返回是否成功
offer(e) -
容器满了,阻塞
put(e) -
容器满了,等待超时退出
offer(e, time, unit)
删除
- 容器没有元素,抛异常
remove() - 返回是否成功
remove(obj)
poll() - 容器没有元素,阻塞
take() - 容器没有元素,等待超时退出
poll(time, unit)
其他
- 获取队头元素,但不出容器,没有则抛异常
element() - 获取队头元素,但不出容器,没有不抛异常
peek()
总结
有界阻塞队列
ArrayBlockingQueue
- 数组实现的有界阻塞队列;
- 队列按照先进先出原则对元素进行排列;
- 默认不保证线程公平访问队列;
- 所谓公平访问队列,是指按照阻塞线程的顺序访问队列。
LinkedBlockingQueue
- 链表实现的有界阻塞队列;
- FIFO
- 未设置容器大小,默认为
Integer.MAX_VALUE
SynchronousQueue
- 不存储元素的有界阻塞队列
- 每一个 put 操作,必须对应另外一个线程的 take 操作
- 用队列实现公平访问;用栈实现不公平访问
LinkedBlockingDequeue
- 双向链表实现的有界阻塞队列
- 双向链表指的是:可从队头、队尾 插入或删除元素
- 未设置容器大小,默认为
Integer.MAX_VALUE
共性
put
操作在容器满时,会阻塞take
操作在容器没有元素时,会阻塞
无界阻塞队列
PriorityBlockingQueue
- 基于 堆 实现的具有 优先级 的无界阻塞队列
- 默认按照自然排序升序排列
- 可通过元素的内部排序,或外部排序自定义顺序
DelayQueue
- 内部基于
PriorityQueue
实现支持延时获取元素的无界阻塞队列 - 队列内元素必须实现
Delayed
接口
LinkedTransferQueue
- 链表实现的无界阻塞队列
- 与其他阻塞队列不同的是,多提供了
transfer
,tryTransfer
方法 transfer
会阻塞到有消费者从队列中消费元素tryTransfer
不带时间的方法,会直接返回;带时间的方法,会根据时间阻塞到有消费者消费
总结
put
,take
均不阻塞
阻塞队列的道与术
上面讲了阻塞队列如何使用,但是那仅仅是术。只有了解了道(阻塞队列的设计),我们才可以在即使是换了语言的情况下,也能模仿 JAVA
阻塞队列的设计思路,自己编写阻塞队列。
问题一:选择合适的数据结构
数据结构,选数组,还是链表都可以。只要清楚数组,链表各自的优缺点,并进行选择即可。
考虑到需要高速存取,不希望有内存碎片。因此,选择数组会相对合适。在考虑到容器的重复使用,我们就需要2 个指针,存放当前消费者消费到哪个位置,生产者最后生产的元素在哪个位置。
其实以上描述的是循环数组的结构。
问题二:容器满了,生产者如何实现不在生产;容器没元素,消费者如何实现不再消费
这里用 伪代码 代码描述该问题。
public void producer() {
while(数组长度 == 容器内的元素个数) {
生产者休眠
}
}
public void consumer() {
while(容器内的元素个数 == 0) {
消费者休眠
}
}
问题三:生产者应该在什么时候唤醒消费者;消费者应该在什么时候唤醒生产者
将上诉代码稍微改造下。我们只需要在 producer
方法被调用后,就可以通知消费者消费了
public void producer() {
while(数组长度 == 容器内的元素个数) {
生产者休眠
}
生产
唤醒消费者
}
public void consumer() {
while(容器内的元素个数 == 0) {
消费者休眠
}
生产
唤醒生产者
}
问题四:如何保护共享资源
对于共享资源的保护,那只能使用锁来保护了
public void producer() {
加锁
while(数组长度 == 容器内的元素个数) {
生产者休眠
}
生产
唤醒消费者
释放锁
}
public void consumer() {
加锁
while(容器内的元素个数 == 0) {
消费者休眠
}
生产
唤醒生产者
释放锁
}
总结
上诉的代码其实是非常经典的多生产者与多消费者的代码。这边做的改造,仅仅是使用循环数组作为存储容器。
参考
- 聊聊Java中的并发队列中 有界队列和无界队列的区别
- 《Java 并发编程的艺术》