PriorityQueue
是 Java 提供的一种基于优先级的队列数据结构。虽然 PriorityQueue
本身不是线程安全的,但它在单线程环境下非常常用。为了在多线程环境中使用,Java 提供了 PriorityBlockingQueue
,它是 PriorityQueue
的线程安全版本。
1. PriorityQueue
的概述
1.1 什么是 PriorityQueue
?
PriorityQueue
是 Java 中实现 Queue
接口的一个类,它是一种基于优先级的队列,队列中的元素按优先级排序。默认情况下,PriorityQueue
是一个最小堆(Min-Heap),即队首元素是队列中最小的元素。当你从 PriorityQueue
中获取元素时,总是返回当前优先级最高(即值最小)的元素。
1.2 PriorityQueue
的主要特性
- 优先级排序:
PriorityQueue
的元素并不是按插入顺序排列的,而是根据元素的自然顺序(或者通过传递给构造函数的比较器)进行排序。 - 不允许
null
元素:PriorityQueue
不允许null
元素,因为null
元素无法确定优先级。 - 动态扩展:
PriorityQueue
的底层是一个基于数组的最小堆,因此它能够根据需要自动扩展其容量。 - 非线程安全:
PriorityQueue
不是线程安全的,如果需要在多线程环境下使用,需要使用外部同步或使用PriorityBlockingQueue
。
1.3 PriorityQueue
的使用示例
下面是一个简单的 PriorityQueue
使用示例:
import java.util.PriorityQueue;
public class PriorityQueueExample {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.add(5);
priorityQueue.add(1);
priorityQueue.add(3);
priorityQueue.add(2);
System.out.println("PriorityQueue elements in order:");
while (!priorityQueue.isEmpty()) {
System.out.println(priorityQueue.poll());
}
}
}
输出结果为:
1
2
3
5
在这个例子中,PriorityQueue
将元素按自然顺序排序(从小到大),并按优先级顺序(最小的元素最先)输出。
2. PriorityQueue
的内部实现
2.1 基于堆的数据结构
PriorityQueue
的内部是通过堆(Heap)数据结构实现的,通常是最小堆。堆是一种特殊的完全二叉树,每个节点的值都小于或等于其子节点的值。
- 最小堆:在最小堆中,堆顶元素是堆中最小的元素。这使得
PriorityQueue
的peek()
和poll()
操作总是返回队列中优先级最高的元素。
2.2 动态数组扩展
PriorityQueue
底层是一个数组,当队列中的元素超过当前数组的容量时,数组会自动扩展。扩展后的新容量通常是旧容量的1.5倍左右。
2.3 插入与删除操作
- 插入操作(
add
或offer
):当向PriorityQueue
插入一个元素时,元素首先被添加到堆的末尾,然后通过向上调整(Up-Heap)操作将该元素移动到正确的位置,以维持堆的性质。 - 删除操作(
poll
):删除堆顶元素时,通常用堆的最后一个元素替换堆顶元素,然后通过向下调整(Down-Heap)操作恢复堆的性质。
2.4 自定义排序
PriorityQueue
默认使用自然顺序(通过元素的 compareTo
方法)对元素排序。如果需要自定义排序,可以在创建 PriorityQueue
时传入一个 Comparator
。
PriorityQueue<String> queue = new PriorityQueue<>((a, b) -> b.compareTo(a));
上面的例子创建了一个基于字符串的反序(降序)优先级队列。
3. 多线程环境中的 PriorityBlockingQueue
3.1 什么是 PriorityBlockingQueue
?
PriorityBlockingQueue
是 PriorityQueue
的线程安全版本,它实现了 BlockingQueue
接口,支持多线程环境下的并发访问。与 PriorityQueue
不同,PriorityBlockingQueue
是无界的,并且支持阻塞的插入和取出操作。
3.2 PriorityBlockingQueue
的主要特性
- 线程安全:通过内部使用的锁机制确保线程安全,多个线程可以同时进行插入和取出操作。
- 无界队列:
PriorityBlockingQueue
是无界的,这意味着它可以容纳任意数量的元素,而不需要指定初始容量。 - 阻塞操作:提供阻塞的
put
和take
方法,当队列为空时,take
操作会阻塞直到有元素可用。
3.3 PriorityBlockingQueue
的使用示例
以下是一个 PriorityBlockingQueue
在多线程环境中的使用示例:
import java.util.concurrent.PriorityBlockingQueue;
public class PriorityBlockingQueueExample {
public static void main(String[] args) {
PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
// Producer thread
Thread producer = new Thread(() -> {
for (int i = 10; i > 0; i--) {
try {
queue.put(i);
System.out.println("Produced: " + i);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
try {
while (true) {
Integer value = queue.take();
System.out.println("Consumed: " + value);
if (value == 1) break; // End loop after consuming 1
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
在这个示例中,生产者线程将整数值按照从大到小的顺序放入 PriorityBlockingQueue
,而消费者线程则以优先级顺序(从小到大)从队列中取出并消费这些值。
4. PriorityQueue
与 PriorityBlockingQueue
的区别
4.1 线程安全性
PriorityQueue
:非线程安全,在多线程环境下需要手动同步。PriorityBlockingQueue
:线程安全,内部实现了锁机制,适合多线程环境。
4.2 阻塞操作
PriorityQueue
:不支持阻塞操作。poll()
在队列为空时立即返回null
。PriorityBlockingQueue
:支持阻塞操作。take()
在队列为空时阻塞直到有元素可用。
4.3 有界与无界
PriorityQueue
:有界或无界,取决于初始化时的容量设定,但默认会自动扩展。PriorityBlockingQueue
:无界,可以容纳任意数量的元素。
5. PriorityQueue
和 PriorityBlockingQueue
的使用场景
5.1 PriorityQueue
的适用场景
- 单线程任务调度:适合在单线程环境下对任务按优先级排序并依次处理。
- 优先级缓存:适合在单线程中管理优先级队列,处理按优先级分配的任务或事件。
5.2 PriorityBlockingQueue
的适用场景
- 多线程任务调度:适合在多线程环境下对任务按优先级排序并由多个线程并发处理。
- 生产者-消费者模型:适合在需要优先级处理任务的生产者-消费者模型中使用,尤其是在多个生产者和消费者线程并发运行的场景中。
6. 总结
PriorityQueue
是 Java 中一个非常有用的基于优先级的队列,在单线程环境中表现出色。然而,在多线程环境下,PriorityQueue
由于非线程安全性,不能直接使用。PriorityBlockingQueue
则是 PriorityQueue
的线程安全版本,适合在多线程环境下使用。
在任务调度、优先级处理等场景中,选择合适的队列类型能够显著提升程序的性能和可靠性。