|
| 1 | + |
| 2 | + |
| 3 | + |
| 4 | +## 队列的实现 |
| 5 | + |
| 6 | +### PriorityQueue |
| 7 | + |
| 8 | +`PriorityQueue` 是两个非主题的 `Queue` 实现之一,主要不是为并发使用而设计的(另一个是 `ArrayDeque`)。它不是线程安全的,也不提供阻塞行为。它根据 `NavigableSet` 所使用的顺序放弃其处理元素 - 如果它们实现 `Comparable` 时其元素的自然顺序,或构造 `PriorityQueue` 时提供的比较器施加的顺序。因此,`PriorityQueue` 将成为我们在 `13.2` 节中使用 `NavigableSet` 概述的基于优先级的待办事项管理器的另一种设计选择(显然,以其名称为例)。您的应用程序将决定选择哪种替代方法:如果需要检查和操作一组等待任务,请使用 `NavigableSet`。如果其主要要求是有效访问要执行的下一个任务,请使用 `PriorityQueue`。 |
| 9 | + |
| 10 | +选择 `PriorityQueue` 允许我们重新考虑排序:因为它容纳重复项,所以它不会共享 `NavigableSet` 对与 `equals` 等效的排序的要求。为了强调这一点,我们将为我们的待办事项经理定义一个仅依赖于优先事项的新订单。与您所期望的相反,`PriorityQueue`不保证它如何呈现具有相同值的多个元素。因此,如果在我们的例子中,几个任务与队列中的最高优先级相关联,那么它将任意选择其中的一个作为头元素。 |
| 11 | + |
| 12 | +`PriorityQueue`的构造函数是: |
| 13 | + |
| 14 | +```java |
| 15 | + PriorityQueue() // 自然排序,默认初始容量(11) |
| 16 | + PriorityQueue(Collection<? extends E> c) // 从c取出的元素的自然顺序,除非c是PriorityQueue或SortedSet,在这种情况下,复制c的顺序 |
| 17 | + PriorityQueue(int initialCapacity) // 自然排序,指定的初始容量 |
| 18 | + PriorityQueue(int initialCapacity, Comparator<? super E> comparator) // 比较器排序,指定初始容量 |
| 19 | + PriorityQueue(PriorityQueue<? extends E> c) // 从c复制的顺序和元素 |
| 20 | + PriorityQueue(SortedSet<? extends E> c) // 从c复制的顺序和元素 |
| 21 | +``` |
| 22 | + |
| 23 | + |
| 24 | + |
| 25 | +图 `14-3`。 将一个元素添加到 `PriorityQueue` |
| 26 | + |
| 27 | +请注意第二个构造函数如何避免第 `13.2.2` 节中讨论的重载 `TreeSet` 构造函数的问题。 我们可以使用 `PriorityQueue` 来简单地实现我们的待办事项管理器,其中使用了第 `13.2` 节中定义的 `PriorityTask` 类,而新的比较器仅取决于任务的优先级: |
| 28 | + |
| 29 | +```java |
| 30 | + final int INITIAL_CAPACITY = 10; |
| 31 | + Comparator<PriorityTask> priorityComp = new Comparator<PriorityTask>() { |
| 32 | + public int compare(PriorityTask o1, PriorityTask o2) { |
| 33 | + return o1.getPriority().compareTo(o2.getPriority()); |
| 34 | + } |
| 35 | + }; |
| 36 | + Queue<PriorityTask> priorityQueue = new PriorityQueue<PriorityTask>(INITIAL_CAPACITY, priorityComp); |
| 37 | + priorityQueue.add(new PriorityTask(mikePhone, Priority.MEDIUM)); |
| 38 | + priorityQueue.add(new PriorityTask(paulPhone, Priority.HIGH)); |
| 39 | + ... |
| 40 | + PriorityTask nextTask = priorityQueue.poll(); |
| 41 | +``` |
| 42 | + |
| 43 | +优先级队列通常通过优先堆有效地实现。一个优先级堆是一个二叉树,有点像我们在 `13.2.2` 节中看到的那样实现 `TreeSet`,但有两点不同:首先,唯一的排序约束是树中的每个节点应该大于它的子节点,其次,除了可能的最低层之外,树的每一层都应该是完整的;如果最低级别不完整,它所包含的节点必须在左侧组合在一起。图 `14-3`(`a`)显示了一个小优先级堆,每个节点只显示包含其优先级的字段。要将一个新元素添加到优先堆中,它首先附加在最左边的空位上,如图 `14-3`(`b`)中圆圈所示。然后,它会与其父级重复交换,直至达到具有更高优先级的父级。在图中,这只需要将新元素与其父元素交换一次,如图 `14-3`(`c`)所示。 (图 `14-3` 和图 `14-4` 中圈出的节点刚好改变位置。) |
| 44 | + |
| 45 | +从优先级堆获取最高优先级的元素是微不足道的:它是树的根。但是,如果删除了这些结果,则必须重新组织这两个单独的树,以重新组织优先堆。这是通过首先将最底层的最右边的元素放到根位置来完成的。然后 - 与添加元素的过程相反 - 它会与其中较大的子元素重复交换,直到它具有比其中任何一个更高的优先级。图 `14-4` 显示了这个过程 - 再次只需要一次交换 - 从头部被移除后,从图 `14-3`(`c`)中的堆开始。 |
| 46 | + |
| 47 | + |
| 48 | + |
| 49 | +图 `14-4`。 删除 `PriorityQueue` 的头部 |
| 50 | + |
| 51 | +除了不变的开销之外,元素的添加和删除都需要一些与树高度成比例的操作。因此,`PriorityQueue` 为 `offer`,`poll`,`remove()` 和 `add` 提供 `O(log n)` 时间。`remove(Object)` 和 `contains` 方法可能需要遍历整个树,所以它们需要 `O(n)` 时间。方法 `peek` 和 `element` 只是在不删除它的情况下检索树的根,它需要一个不变的时间,就像 `size` 一样,它使用一个不断更新的对象字段。 |
| 52 | + |
| 53 | +`PriorityQueue` 不适合并发使用。它的迭代器是快速失败的,它不支持客户端锁定。线程安全版本 `PriorityBlockingQueue`(请参阅第 `14.3.2` 节)。 |
| 54 | + |
| 55 | +### ConcurrentLinkedQueue |
| 56 | + |
| 57 | +另一个非阻塞队列实现是 `ConcurrentLinkedQueue`,它是一个无界的,线程安全的 `FIFO` 排序队列。它使用链接结构,类似于我们在第 `13.2.3` 节中看到的链接结构作为跳过列表的基础,在 `13.1.1` 节中用于散列表溢出链接。我们在那里注意到,链接结构的主要吸引力之一是通过指针重排实现的插入和移除操作在不变的时间内执行。这使得它们作为队列实现特别有用,其中这些操作总是在结构末端的单元上需要 - 也就是说,不需要使用链接结构的缓慢顺序搜索来定位单元。 |
| 58 | + |
| 59 | +`ConcurrentLinkedQueue` 使用基于 `CAS` 的无等待算法 - 也就是说,无论其他线程访问队列的状态如何,都可以确保任何线程始终可以完成其当前操作。它在一段时间内执行队列插入和删除操作,但需要线性时间来执行大小。这是因为依赖于线程之间的插入和移除协作的算法没有跟踪队列大小,并且必须在需要时迭代队列以计算它。 |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | +图 `14-5`。`BlockingQueue` |
| 64 | + |
| 65 | +`ConcurrentLinkedQueue` 有 `12.3` 节中讨论的两个标准构造函数。它的迭代器很弱一致。 |
| 66 | + |
0 commit comments