Java 集合框架3:Queue

Queue

1.概述

Queue 是一个保持将要处理元素的集合,基于 Collection 的方法,还提供添加、抽取、读取方法,而这些方法都有两个版本,一个是针对有长度限制的实现而可能抛出异常的版本,另一个是针对无长度限制的实现而不会抛出异常的版本:

Queue 通常以 FIFO 形式组织元素, stack 以 LIFO 形式组织元素。所以,Queue 的实现需要指定排序规则,使用自然排序或者提供 comparator。

无论以 FIFO 还是 LIFO 组织元素,Queue 的 remove 和 poll 均移除 head 元素,element 和 peek 也是返回 head 元素。

Queue 的某些类允许插入 null 元素,比如 ArrayList。但使用时,应该不允许插入 null,以区分 Queue 方法中不会抛出异常而是返回 null 的函数的调用情况。

2.BlockingQueue

Queue 并未定义阻塞方法,但 Queue 的子接口 BlockingQueue 提供了阻塞。

BlockingQueue 提供 4 种方法,前两种与 Queue 一样,第 3 种会阻塞当前的线程直到操作可以成功,最后一种则是阻塞一段时间,如果没有成功就放弃操作。

BlockingQueue 不支持 null 元素,主要原因是 null 元素在 poll 方法中用作哨兵。

BlockingQueue 主要用于生产者-消费者队列。

BlockingQueue 是线程安全的,所有排队方法以原子方式实现其效果,但是 bulk 操作(addAll、containsAll、retainAll 和 removeAll)不一定以原子操作进行。

BlockingQueue 不支持关闭操作(close、shutdown),通常使用插入特殊值(end-of-stream、poison objects)来表明队列关闭

使用示例

生产者-消费者问题:

class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) { queue = q; }
public void run() {
try {
while (true) { queue.put(produce()); }
} catch (InterruptedException ex) { ... handle ...}
}
Object produce() { ... }
}
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
public void run() {
try {
while (true) { consume(queue.take()); }
} catch (InterruptedException ex) { ... handle ...}
}
void consume(Object x) { ... }
}
class Setup {
void main() {
BlockingQueue q = new SomeQueueImplementation();
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}

3.Deque

Deque,被称之为双端队列,是 Queue 的子接口。与 Queue 不同的是,它支持从两端进行元素的操作。

与 Queue 一样,Deque 也有两种形式的方法,一种针对于有大小限制的实现,会抛出异常,另一种针对于无大小限制的实现而返回 null 或其他值。

Deque 使用可以退化为 Queue,遵循 FIFO,方法对应关系如下:

Deque 也可以退化为 stack,遵循 LIFO,方法对应如下:

除此之外,Deque 还提供了 removeFirstOccurrence 和 removeLastOccurrence 方法,用于 remove 队列中第一次出现和最后一次出现的指定元素。

Deque 并不支持像 List 那样的索引访问。Deque 不推荐去添加 null 元素,因为 null 元素被指示为队列为空。

4.实现

Queue 的通用实现有 LinkedList 和 PriorityQueue,LinkedList 也是 Deque 的通用实现。(LinkedList 实现参见我的另外一篇博客:Java 集合框架2:List。)

对于 Queue 的并发实现,在 java.util.concurrent 包中,主要有 LinkedBlockingQueue、ArrayBlockingQueue。

Deque 的通用实现还有 ArrayDeque,并发实现有 LinkedBlockingDeque。

PriorityQueue

PriorityQueue 基于堆实现,并且没有边界限制。

PriorityQueue 的迭代器顺序并不保证,如果需要,使用 Arrays 的 sort 方法:

PriorityQueue 并不保证线程安全,如果需要,使用 PriorityBlockingQueue。

实现原理

PriorityQueue 实现基于平衡二叉树,并且采用小顶堆,表明父节点不大于子节点。

以 offer 方法为例,添加元素,首先需要保证有足够的空间,没有则调用 grow 方法进行扩容。然后调用 siftUp 函数添加元素:

扩容策略为小长度时变为 2 倍,否则增长 50%:

siftUp 会根据采用定制排序还是自然排序而调用相应的函数:

这里以自然排序为例,如果新添节点比父节点大,就在新添元素指定的 index 位置 k 添加,否则 k 位置会被填入它的父节点,然后 k 更新为父节点的位置,继续与其该位置的父节点进行比较,直到满足新添元素不小于 k 的父节点为止。这里有一个巧妙的地方是,如果 k 最终变为了根节点,也就是 0,那么它的 parent 也会是 0,此时相等,新元素填充。

对于删减函数,以 remove 为例。

对于 remove 函数,首先会在数组中找寻第一个指定的元素的 index,随后调用 removeAt 函数对对应 index 进行删除:

removeAt 函数中,如果删除的是数组最后一个元素,直接将最后以后一个元素置为 null,并返回。否则,使用 moved 记录最后一个元素,将最后一个元素置为 null 后,需要对原二叉树重新构造,构造调用了 siftDown 函数。

siftDown 函数根据排序规则选择不同的函数,这里以自然排序时调用 siftDownComparable 方法为例,siftDownComparable 进行了第一次结构修改,这一步修改只保证删除位置的节点,也就是 i(siftDownComparable 中为 k),是不大于子节点的

由于底层的二叉树是完全二叉树,那么 size 的一半(该 size 已经在 removeAt 函数中减 1),也就是 half,大于等于它的节点为叶节点。

倘若 k(也就是要 remove 元素在数组中的索引)节点为叶节点(k >= half),那么直接将该叶节点换为 key(也就是原数组中最后一个元素,保存在 moved 的那个)。

倘若 k 不为叶节点(k < half),会进行 while 循环。k 需要和它的最小子节点进行比较(这里先假设了左子节点更小,然后和右节点比较),c 保存了更小的节点。k 不大于 c,表明满足第一次结构修改,则退出循环,将 k 位置填充为 key。如果 k 大于 c,那么 k 和 c 互换,继续循环,直到 k 到达叶节点或 k 不大于 c 时为止。

siftDownComparable 执行完成后,返回到 removeAt 函数,执行第二次结构修改,这里修改的目的是保证删除位置的节点,也就是 i(不同于 siftDownComparable 中的 k,这个 i 是原本删除元素的位置),是不小于父节点的。这里调用了 offer 方法同样会调用到的 siftUp 方法。

LinkedBlockingQueue

LinkedBlockingQueue 是一种可选容量限制的基于链表实现的阻塞队列。

LinkedBlockingQueue 通常比 ArrayBlockingQueue 有更多的修改操作,但更少的查询操作。

LinkedBlockingQueue 包含一个空头节点,默认的容量大小为 Integer.MAX_VALUE。LinkedBlockingQueue使用了 head 和 last 分别指向空头节点和尾节点。

以 put 方法为例,先对添加元素进行上锁,这里上锁采用了 lockInterruptibly 方法,意味着 put 方法支持中断,如果在获取 putLock 的过程中,线程被中断,那么将会抛出 InterruptedException。随后,循环等待队列不为空,循环等待在 count 不被上锁时也能使用,因为我们之前上锁了 put,队列中的元素不会增加。等待到 notFull 后,进行入队 enqueue,然后 put 解锁。在此过程中,c 的作用是便于发送队列不为空等信号。

添加了时间限制参数的 offer 方法与之类似,只是在 notFull 的 await 过程中,判断是否超时:

LinkedBlockingQueue 提供弱一致性的迭代器,因为,获取 iterator 的实例化 Itr 的过程中,进行上锁时,可能其它线程正在使用,等待过程中元素可能已经发生了改变。

ArrayBlockingQueue

ArrayBlockingQueue 是一个基于数组的 Bounded Buffer 实现。

ArrayBlockingQueue 的阻塞实现类似于 LinkedBlockingQueue,只不过它操作的是数组而不是链表。

需要注意的是,存储元素用的数组 items 是用 final 修饰的,意味着 ArrayBlockingQueue 的大小在构造时指定,且不能改变。

posted @   MeYokYang  阅读(88)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示