队列
一、队列概述
队列是一种遵循先进先出(FIFO,First-In-First-Out)原则的重要数据结构。这种特性使其广泛应用于任务调度、消息传递和缓冲处理等场景。典型应用包括操作系统的进程调度、打印机任务队列以及网络请求处理等,这些场景都需要队列来确保数据处理的有序性。
Java集合框架在java.util包中提供了完整的队列实现。Queue接口作为所有队列类的基础,不仅继承了Collection接口的基本功能,还针对队列操作进行了扩展。该接口定义了两类处理方式:一类在操作失败时抛出异常,另一类则返回特殊值(如null或false),以满足不同场景的需求。
1.1 队列的核心操作
Queue接口定义的核心操作如下表所示:
| 操作类型 | 抛出异常的方法 | 返回特殊值的方法 | 说明 |
|---|---|---|---|
| 入队 | add(e) | offer(e) | 将元素添加到队列尾部,成功返回true |
| 出队 | remove() | poll() | 移除并返回队列头部元素,队列为空时前者抛NoSuchElementException,后者返回null |
| 查看队头 | element() | peek() | 返回但不移除队列头部元素,队列为空时前者抛NoSuchElementException,后者返回null |
从表中可以看出,add()和offer()的主要区别在于当队列有界(即有容量限制)且已满时,add()会抛出IllegalStateException,而offer()则返回false。因此,在使用有界队列时,offer()方法更为常用,因为它可以通过返回值判断操作是否成功。
1.2 队列的分类
根据不同的特性,Java中的队列可以分为以下几类:
- 普通队列:遵循FIFO原则,如
LinkedList、ArrayDeque(作为队列使用时)。 - 优先级队列:元素按照优先级排序,而非插入顺序,如
PriorityQueue。 - 双端队列:允许在队列的两端进行元素的插入和删除操作,如
ArrayDeque、LinkedBlockingDeque。 - 阻塞队列:当队列满时,入队操作会阻塞;当队列空时,出队操作会阻塞,主要用于多线程环境,如
LinkedBlockingQueue、ArrayBlockingQueue。 - 并发队列:线程安全的队列,支持多线程并发操作,如
ConcurrentLinkedQueue、LinkedTransferQueue。 - 延迟队列:元素只有在指定的延迟时间后才能被取出,如
DelayQueue。
接下来,我们将详细讲解Java中各个队列相关的类及其方法,包括它们的底层实现、特性、常用方法及使用场景。
二、Queue接口详解
Queue接口是Java队列体系的基础,它继承自Collection接口,因此具备Collection的所有方法(如size()、isEmpty()、contains()等),同时又定义了队列特有的操作方法。
2.1 Queue接口的定义
Queue接口的定义如下:
public interface Queue<E> extends Collection<E> {
// 入队,失败抛异常
boolean add(E e);
// 入队,失败返回false
boolean offer(E e);
// 出队,失败抛异常
E remove();
// 出队,失败返回null
E poll();
// 查看队头,失败抛异常
E element();
// 查看队头,失败返回null
E peek();
}
从定义可以看出,Queue接口是一个泛型接口,类型参数E表示队列中元素的类型。
2.2 Queue接口的方法详解
2.2.1 入队方法
-
boolean add(E e)- 功能:将指定元素添加到队列的尾部。
- 特点:如果队列有容量限制且已满,会抛出
IllegalStateException;如果元素为null且队列不允许null元素,会抛出NullPointerException;如果元素的类不允许加入队列,会抛出ClassCastException;如果元素的某些属性不允许加入队列,会抛出IllegalArgumentException。 - 返回值:操作成功返回
true(根据Collection.add()的规范)。
-
boolean offer(E e)- 功能:将指定元素添加到队列的尾部。
- 特点:与
add()类似,但当队列满时返回false而非抛出异常,其他异常情况与add()一致。 - 返回值:操作成功返回
true,失败返回false。
2.2.2 出队方法
-
E remove()- 功能:移除并返回队列的头部元素。
- 特点:如果队列为空,会抛出
NoSuchElementException。 - 返回值:队列的头部元素。
-
E poll()- 功能:移除并返回队列的头部元素。
- 特点:如果队列为空,返回
null。 - 返回值:队列的头部元素,队列为空时返回
null。
2.2.3 查看队头方法
-
E element()- 功能:返回但不移除队列的头部元素。
- 特点:如果队列为空,会抛出
NoSuchElementException。 - 返回值:队列的头部元素。
-
E peek()- 功能:返回但不移除队列的头部元素。
- 特点:如果队列为空,返回
null。 - 返回值:队列的头部元素,队列为空时返回
null。
2.3 Queue接口的子接口
Queue接口有多个子接口,它们在Queue的基础上扩展了更多特性:
Deque:双端队列接口,允许在队列的两端进行元素的插入和删除操作。BlockingQueue:阻塞队列接口,定义了在多线程环境下的阻塞操作。TransferQueue:继承自BlockingQueue,增加了元素传递的功能,允许生产者直接将元素传递给消费者。
这些子接口将在后续章节中详细讲解。
三、普通队列实现类
3.1 LinkedList
LinkedList是Java集合框架中一个常用的类,它不仅实现了List接口,还实现了Deque接口(进而实现了Queue接口),因此可以作为队列使用。LinkedList底层基于双向链表实现,这使得它在插入和删除元素时具有较高的效率,但随机访问元素的效率较低。
3.1.1 LinkedList的特性
- 数据结构:双向链表,每个节点包含前驱节点引用、后继节点引用和节点值。
- 容量:无界,理论上可以无限添加元素(受限于内存)。
- 线程安全性:非线程安全,多线程环境下需要外部同步。
- 元素允许:允许
null元素,允许重复元素。 - 排序:不保证元素的排序,按照插入顺序维护元素。
3.1.2 LinkedList的构造方法
LinkedList提供了两个构造方法:
-
public LinkedList():创建一个空的LinkedList。Queue<String> queue = new LinkedList<>(); -
public LinkedList(Collection<? extends E> c):创建一个包含指定集合元素的LinkedList,元素的顺序与集合的迭代器返回顺序一致。List<String> list = Arrays.asList("a", "b", "c"); Queue<String> queue = new LinkedList<>(list);
3.1.3 LinkedList作为队列的常用方法
除了实现Queue接口的add()、offer()、remove()、poll()、element()、peek()方法外,LinkedList还继承了Collection和List接口的方法,以下是一些常用方法:
-
int size():返回队列中的元素个数。Queue<String> queue = new LinkedList<>(); queue.add("a"); queue.add("b"); System.out.println(queue.size()); // 输出:2 -
boolean isEmpty():判断队列是否为空。Queue<String> queue = new LinkedList<>(); System.out.println(queue.isEmpty()); // 输出:true queue.add("a"); System.out.println(queue.isEmpty()); // 输出:false -
boolean contains(Object o):判断队列是否包含指定元素。Queue<String> queue = new LinkedList<>(); queue.add("a"); System.out.println(queue.contains("a")); // 输出:true System.out.println(queue.contains("b")); // 输出:false -
Iterator<E> iterator():返回队列的迭代器,用于遍历元素。Queue<String> queue = new LinkedList<>(); queue.add("a"); queue.add("b"); Iterator<String> iterator = queue.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); // 依次输出:a、b } -
Object[] toArray():将队列中的元素转换为数组。Queue<String> queue = new LinkedList<>(); queue.add("a"); queue.add("b"); Object[] array = queue.toArray(); System.out.println(Arrays.toString(array)); // 输出:[a, b] -
boolean remove(Object o):移除队列中第一个出现的指定元素(如果存在)。Queue<String> queue = new LinkedList<>(); queue.add("a"); queue.add("b"); queue.add("a"); queue.remove("a"); System.out.println(queue); // 输出:[b, a] -
boolean addAll(Collection<? extends E> c):将指定集合中的所有元素添加到队列尾部。Queue<String> queue = new LinkedList<>(); queue.add("a"); List<String> list = Arrays.asList("b", "c"); queue.addAll(list); System.out.println(queue); // 输出:[a, b, c] -
void clear():清空队列中的所有元素。Queue<String> queue = new LinkedList<>(); queue.add("a"); queue.add("b"); queue.clear(); System.out.println(queue.isEmpty()); // 输出:true
3.1.4 LinkedList的底层实现原理
LinkedList的底层是一个双向链表,其节点定义如下(简化版):
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
每个节点包含三个部分:存储的元素(item)、前驱节点引用(prev)和后继节点引用(next)。LinkedList类中维护了两个引用:first指向链表的头节点,last指向链表的尾节点。
当调用add(E e)或offer(E e)方法时,会在链表的尾部添加一个新节点:
public boolean add(E e) {
linkLast(e);
return true;
}
public boolean offer(E e) {
return add(e); // LinkedList的offer()方法直接调用add(),因为它是无界的
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
可以看出,LinkedList的offer()方法与add()方法实现相同,因为LinkedList是无界队列,不会出现添加失败的情况(除非内存不足,此时会抛出OutOfMemoryError)。
当调用remove()方法时,会移除并返回头节点的元素:
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// 获取头节点的元素
final E element = f.item;
// 获取头节点的后继节点
final Node<E> next = f.next;
// 释放头节点的引用,帮助GC
f.item = null;
f.next = null; // 方便GC
// 将first指向后继节点
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
当调用poll()方法时,与remove()类似,但队列为空时返回null:
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
element()和peek()方法用于查看头节点元素,实现如下:
public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
3.1.5 LinkedList的使用场景
LinkedList作为队列使用时,适用于以下场景:
- 需要频繁进行插入和删除操作,而随机访问操作较少的场景。
- 对队列的容量没有限制(或限制较小)的场景。
- 单线程环境或可以通过外部同步保证线程安全的多线程环境。
例如,在实现一个简单的任务调度器时,可以使用LinkedList作为任务队列,不断将新任务加入队列尾部,调度线程从队列头部取出任务执行。
import java.util.LinkedList;
import java.util.Queue;
public class TaskScheduler {
private Queue<Task> taskQueue = new LinkedList<>();
// 添加任务到队列
public void addTask(Task task) {
taskQueue.offer(task);
}
// 执行队列中的任务
public void executeTasks() {
while (!taskQueue.isEmpty()) {
Task task = taskQueue.poll();
task.execute();
}
}
public static void main(String[] args) {
TaskScheduler scheduler = new TaskScheduler();
scheduler.addTask(new Task("任务1"));
scheduler.addTask(new Task("任务2"));
scheduler.addTask(new Task("任务3"));
scheduler.executeTasks();
}
static class Task {
private String name;
public Task(String name) {
this.name = name;
}
public void execute() {
System.out.println("执行任务:" + name);
}
}
}
运行上述代码,输出结果为:
执行任务:任务1
执行任务:任务2
执行任务:任务3
可以看到,任务按照添加的顺序依次执行,符合队列的FIFO特性。
3.2 ArrayDeque(作为队列使用)
ArrayDeque是Java中另一个实现了Deque接口的类,它底层基于动态数组实现,既可以作为双端队列使用,也可以作为普通队列使用。与LinkedList相比,ArrayDeque在元素的插入、删除和访问操作上通常具有更高的效率,因为数组的访问是连续的内存空间,缓存利用率更高。
3.2.1 ArrayDeque的特性
- 数据结构:动态数组,当数组容量不足时会自动扩容。
- 容量:无界,理论上可以无限添加元素(受限于内存)。
- 线程安全性:非线程安全,多线程环境下需要外部同步。
- 元素允许:不允许
null元素,否则会抛出NullPointerException;允许重复元素。 - 排序:不保证元素的排序,按照插入顺序维护元素(作为队列使用时)。
3.2.2 ArrayDeque的构造方法
ArrayDeque提供了三个构造方法:
-
public ArrayDeque():创建一个初始容量为16的空ArrayDeque。Queue<String> queue = new ArrayDeque<>(); -
public ArrayDeque(int numElements):创建一个初始容量至少为指定值的空ArrayDeque。注意,实际容量可能是大于numElements的最小的2的幂次方。Queue<String> queue = new ArrayDeque<>(10); // 初始容量为16(因为16是大于10的最小2的幂次方) -
public ArrayDeque(Collection<? extends E> c):创建一个包含指定集合元素的ArrayDeque,元素的顺序与集合的迭代器返回顺序一致。List<String> list = Arrays.asList("a", "b", "c"); Queue<String> queue = new ArrayDeque<>(list);
3.2.3 ArrayDeque作为队列的常用方法
ArrayDeque实现了Queue接口的所有方法,同时也有一些自身特有的方法。以下是作为队列使用时的常用方法:
-
boolean add(E e):将元素添加到队列尾部,由于ArrayDeque是无界的,此方法始终返回true,如果元素为null则抛出NullPointerException。Queue<String> queue = new ArrayDeque<>(); queue.add("a"); queue.add("b"); System.out.println(queue); // 输出:[a, b] -
boolean offer(E e):与add()方法功能相同,将元素添加到队列尾部,返回true,元素为null时抛出NullPointerException。Queue<String> queue = new ArrayDeque<>(); queue.offer("a"); queue.offer("b"); System.out.println(queue); // 输出:[a, b] -
E remove():移除并返回队列头部元素,队列为空时抛出NoSuchElementException。Queue<String> queue = new ArrayDeque<>(); queue.add("a"); queue.add("b"); System.out.println(queue.remove()); // 输出:a System.out.println(queue); // 输出:[b] -
E poll():移除并返回队列头部元素,队列为空时返回null。Queue<String> queue = new ArrayDeque<>(); queue.add("a"); queue.add("b"); System.out.println(queue.poll()); // 输出:a System.out.println(queue); // 输出:[b] -
E element():返回但不移除队列头部元素,队列为空时抛出NoSuchElementException。Queue<String> queue = new ArrayDeque<>(); queue.add("a"); System.out.println(queue.element()); // 输出:a -
E peek():返回但不移除队列头部元素,队列为空时返回null。Queue<String> queue = new ArrayDeque<>(); queue.add("a"); System.out.println(queue.peek()); // 输出:a -
其他继承自
Collection接口的方法如size()、isEmpty()、contains()等与LinkedList类似,此处不再赘述。
3.2.4 ArrayDeque的底层实现原理
ArrayDeque底层使用一个动态数组elements来存储元素,同时维护了两个索引:head(指向队列的头部元素)和tail(指向队列尾部元素的下一个位置)。数组的长度始终是2的幂次方,这是为了便于使用位运算来实现循环队列的效果。
当调用add(E e)或offer(E e)方法时,会将元素添加到tail指向的位置,然后更新tail索引:
public boolean add(E e) {
offer(e);
return true;
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
// 计算新的tail索引,使用位运算实现循环
if ( (tail = (tail + 1) & (elements.length - 1)) == head )
doubleCapacity(); // 数组已满,扩容
return true;
}
上述代码中,(tail + 1) & (elements.length - 1)是一种高效的取模运算,由于elements.length是2的幂次方,elements.length - 1的二进制表示全为1,因此与tail + 1进行与运算等价于对elements.length取模,实现了循环队列的效果。当tail与head相等时,表示数组已满,此时调用doubleCapacity()方法进行扩容。
doubleCapacity()方法的实现如下:
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // 头部元素到数组末尾的元素个数
int newCapacity = n << 1; // 容量翻倍
if (newCapacity < 0)
throw new IllegalStateException("Deque too big");
Object[] a = new Object[newCapacity];
// 复制从head到数组末尾的元素
System.arraycopy(elements, p, a, 0, r);
// 复制从数组开始到head的元素
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}
扩容时,将原数组的容量翻倍(左移一位),然后将原数组中的元素复制到新数组中,调整head为0,tail为原数组的长度。
当调用poll()方法时,会移除并返回head指向的元素,然后更新head索引:
public E poll() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// 如果队列为空,返回null
if (result == null)
return null;
elements[h] = null; // 帮助GC
// 更新head索引
head = (h + 1) & (elements.length - 1);
return result;
}
remove()方法的实现与poll()类似,但队列为空时会抛出异常:
public E remove() {
E x = poll();
if (x == null)
throw new NoSuchElementException();
return x;
}
peek()方法用于查看头部元素:
public E peek() {
return (E) elements[head];
}
element()方法则在peek()返回null时抛出异常:
public E element() {
E x = peek();
if (x == null)
throw new NoSuchElementException();
return x;
}
3.2.5 ArrayDeque与LinkedList的对比
| 特性 | ArrayDeque | LinkedList |
|---|---|---|
| 数据结构 | 动态数组 | 双向链表 |
| 内存占用 | 较低(连续内存) | 较高(每个节点有额外引用) |
| 插入/删除效率 | 高(数组操作,除非需要扩容) | 高(链表节点操作) |
| 随机访问效率 | 高(O(1)) | 低(O(n)) |
| 是否允许null元素 | 不允许 | 允许 |
| 迭代器性能 | 高 | 较高 |
从对比可以看出,ArrayDeque在大多数情况下性能优于LinkedList,尤其是在频繁的插入、删除和访问操作中。因此,在不需要存储null元素且需要作为队列或双端队列使用时,ArrayDeque是更好的选择。
3.2.6 ArrayDeque的使用场景
ArrayDeque作为队列使用时,适用于以下场景:
- 需要高效的插入、删除和访问操作的场景。
- 不需要存储
null元素的场景。 - 单线程环境或可以通过外部同步保证线程安全的多线程环境。
例如,在实现一个简单的缓存系统时,可以使用ArrayDeque作为缓存队列,当缓存满时移除最早加入的元素:
import java.util.ArrayDeque;
import java.util.Queue;
public class SimpleCache {
private Queue<String> cacheQueue = new ArrayDeque<>(3); // 缓存容量为3
private static final int MAX_CAPACITY = 3;
// 添加元素到缓存
public void addToCache(String item) {
if (cacheQueue.size() >= MAX_CAPACITY) {
// 缓存满,移除最早加入的元素
cacheQueue.poll();
}
cacheQueue.offer(item);
}
// 打印缓存中的元素
public void printCache() {
System.out.println("缓存中的元素:" + cacheQueue);
}
public static void main(String[] args) {
SimpleCache cache = new SimpleCache();
cache.addToCache("元素1");
cache.printCache(); // 输出:缓存中的元素:[元素1]
cache.addToCache("元素2");
cache.printCache(); // 输出:缓存中的元素:[元素1, 元素2]
cache.addToCache("元素3");
cache.printCache(); // 输出:缓存中的元素:[元素1, 元素2, 元素3]
cache.addToCache("元素4");
cache.printCache(); // 输出:缓存中的元素:[元素2, 元素3, 元素4]
}
}
运行上述代码,输出结果符合预期,展示了ArrayDeque作为队列在缓存场景中的应用。
四、优先级队列(PriorityQueue)
PriorityQueue是Java中实现优先级队列的类,它基于优先级堆(通常是最小堆)实现,元素按照优先级进行排序,而非插入顺序。在PriorityQueue中,每次出队的元素都是当前队列中优先级最高的元素。
4.1 PriorityQueue的特性
- 数据结构:优先级堆(数组实现的二叉堆)。
- 容量:无界,默认初始容量为11,当元素数量超过容量时会自动扩容。
- 线程安全性:非线程安全,多线程环境下需要外部同步(或使用
PriorityBlockingQueue)。 - 元素允许:不允许
null元素;如果元素没有实现Comparable接口且未指定比较器,会抛出ClassCastException;允许重复元素,但排序时会根据优先级处理。 - 排序方式:默认按照元素的自然顺序(实现
Comparable接口)排序,也可以通过构造方法指定Comparator来定义排序规则。
4.2 PriorityQueue的构造方法
PriorityQueue提供了多个构造方法:
-
public PriorityQueue():创建一个初始容量为11的空优先级队列,按照元素的自然顺序排序。PriorityQueue<Integer> pq = new PriorityQueue<>(); -
public PriorityQueue(int initialCapacity):创建一个指定初始容量的空优先级队列,按照元素的自然顺序排序。initialCapacity必须大于0,否则抛出IllegalArgumentException。PriorityQueue<Integer> pq = new PriorityQueue<>(20); -
public PriorityQueue(Comparator<? super E> comparator):创建一个初始容量为11的空优先级队列,按照指定的比较器排序。// 创建一个按照降序排序的优先级队列 PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder()); -
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator):创建一个指定初始容量和比较器的空优先级队列。PriorityQueue<Integer> pq = new PriorityQueue<>(20, Collections.reverseOrder()); -
public PriorityQueue(Collection<? extends E> c):创建一个包含指定集合元素的优先级队列。如果集合是SortedSet或另一个PriorityQueue,则按照其排序规则排序;否则,按照元素的自然顺序排序。List<Integer> list = Arrays.asList(3, 1, 2); PriorityQueue<Integer> pq = new PriorityQueue<>(list); -
public PriorityQueue(PriorityQueue<? extends E> c):创建一个包含指定PriorityQueue元素的优先级队列,使用与指定队列相同的排序规则。PriorityQueue<Integer> pq1 = new PriorityQueue<>(); pq1.add(3); pq1.add(1); pq1.add(2); PriorityQueue<Integer> pq2 = new PriorityQueue<>(pq1); -
public PriorityQueue(SortedSet<? extends E> c):创建一个包含指定SortedSet元素的优先级队列,使用与指定集合相同的排序规则。SortedSet<Integer> set = new TreeSet<>(); set.add(3); set.add(1); set.add(2); PriorityQueue<Integer> pq = new PriorityQueue<>(set);
4.3 PriorityQueue的常用方法
PriorityQueue实现了Queue接口的所有方法,同时还有一些与排序和堆操作相关的方法。以下是常用方法的详细说明:
4.3.1 入队方法
-
boolean add(E e):将元素添加到优先级队列中,成功返回true。如果元素为null,抛出NullPointerException;如果元素无法比较(未实现Comparable且无比较器),抛出ClassCastException。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(3); pq.add(1); pq.add(2); System.out.println(pq); // 输出:[1, 3, 2](内部存储结构,并非排序后的完整列表) -
boolean offer(E e):与add()方法功能相同,将元素添加到优先级队列中,返回true。由于PriorityQueue是无界的,此方法始终返回true。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.offer(3); pq.offer(1); pq.offer(2);
4.3.2 出队方法
-
E remove():移除并返回队列中优先级最高的元素(队头元素),队列为空时抛出NoSuchElementException。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(3); pq.add(1); pq.add(2); System.out.println(pq.remove()); // 输出:1 System.out.println(pq); // 输出:[2, 3] -
E poll():移除并返回队列中优先级最高的元素,队列为空时返回null。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(3); pq.add(1); pq.add(2); System.out.println(pq.poll()); // 输出:1 System.out.println(pq); // 输出:[2, 3]
4.3.3 查看队头方法
-
E element():返回但不移除队列中优先级最高的元素,队列为空时抛出NoSuchElementException。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(3); pq.add(1); pq.add(2); System.out.println(pq.element()); // 输出:1 -
E peek():返回但不移除队列中优先级最高的元素,队列为空时返回null。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(3); pq.add(1); pq.add(2); System.out.println(pq.peek()); // 输出:1
4.3.4 其他常用方法
-
int size():返回队列中的元素个数。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(1); pq.add(2); System.out.println(pq.size()); // 输出:2 -
boolean isEmpty():判断队列是否为空。PriorityQueue<Integer> pq = new PriorityQueue<>(); System.out.println(pq.isEmpty()); // 输出:true -
boolean contains(Object o):判断队列是否包含指定元素。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(1); System.out.println(pq.contains(1)); // 输出:true System.out.println(pq.contains(2)); // 输出:false -
Iterator<E> iterator():返回队列的迭代器,用于遍历元素。注意,迭代器返回的元素顺序不一定是优先级顺序。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(3); pq.add(1); pq.add(2); Iterator<Integer> iterator = pq.iterator(); while (iterator.hasNext()) { System.out.print(iterator.next() + " "); // 可能输出:1 3 2(顺序不固定) } -
Object[] toArray():将队列中的元素转换为数组,数组中的元素顺序不一定是优先级顺序。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(3); pq.add(1); pq.add(2); Object[] array = pq.toArray(); System.out.println(Arrays.toString(array)); // 可能输出:[1, 3, 2] -
boolean remove(Object o):移除队列中第一个出现的指定元素(如果存在),返回true。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(3); pq.add(1); pq.add(2); pq.remove(3); System.out.println(pq); // 输出:[1, 2] -
void clear():清空队列中的所有元素。PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(1); pq.add(2); pq.clear(); System.out.println(pq.isEmpty()); // 输出:true
4.4 PriorityQueue的底层实现原理
PriorityQueue底层基于二叉堆实现,二叉堆是一种完全二叉树,它可以分为最大堆和最小堆。PriorityQueue默认是最小堆,即每个父节点的值小于或等于其子节点的值,根节点(堆顶)是整个堆中最小的元素。
二叉堆通常使用数组来存储,对于数组中的索引为i的元素:
- 其父节点的索引为
(i - 1) >>> 1(无符号右移一位,等价于(i - 1) / 2)。 - 其左子节点的索引为
2 * i + 1。 - 其右子节点的索引为
2 * i + 2。
PriorityQueue的核心字段包括:
transient Object[] queue:存储元素的数组。int size:队列中的元素个数。private final Comparator<? super E> comparator:用于比较元素的比较器,null表示使用元素的自然顺序。transient int modCount:用于快速失败机制的修改计数。
4.4.1 入队操作(offer()方法)
当调用offer(E e)方法添加元素时,会执行以下步骤:
- 检查元素是否为
null,如果是则抛出NullPointerException。 - 检查是否需要扩容(如果
size等于数组长度)。 - 将元素添加到数组的末尾(索引为
size的位置)。 - 调用
siftUp()方法调整堆结构,以维持堆的特性。 - 增加
size的值。
offer()方法的实现如下:
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1); // 扩容
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e); // 调整堆
return true;
}
siftUp()方法用于将新添加的元素向上调整,以维持堆的特性:
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
// 使用元素的自然顺序(Comparable)调整
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1; // 父节点索引
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break; // 新元素大于等于父节点,停止调整
queue[k] = e; // 将父节点下移
k = parent; // 继续向上调整
}
queue[k] = key;
}
// 使用指定的比较器调整
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
siftUp()方法的过程是:将新元素与父节点比较,如果新元素小于父节点(对于最小堆),则交换它们的位置,然后继续与新的父节点比较,直到新元素大于等于父节点或到达根节点。
4.4.2 出队操作(poll()方法)
当调用poll()方法移除元素时,会执行以下步骤:
- 如果队列为空,返回
null。 - 获取根节点元素(优先级最高的元素)作为返回值。
- 将数组末尾的元素移到根节点位置。
- 调用
siftDown()方法调整堆结构,以维持堆的特性。 - 减少
size的值,并将原末尾元素的位置设为null(帮助GC)。
poll()方法的实现如下:
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0]; // 根节点元素
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x); // 调整堆
return result;
}
siftDown()方法用于将根节点的元素向下调整,以维持堆的特性:
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
// 使用元素的自然顺序调整
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
int half = size >>> 1; // 非叶子节点的最大索引
while (k < half) {
int child = (k << 1) + 1; // 左子节点索引
Object c = queue[child];
int right = child + 1; // 右子节点索引
if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right]; // 选择左右子节点中较小的一个
if (key.compareTo((E) c) <= 0)
break; // 父节点小于等于子节点,停止调整
queue[k] = c; // 将子节点上移
k = child; // 继续向下调整
}
queue[k] = key;
}
// 使用指定的比较器调整
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
siftDown()方法的过程是:将当前节点与左右子节点中较小的一个比较,如果当前节点大于该子节点,则交换它们的位置,然后继续与新的子节点比较,直到当前节点小于等于子节点或到达叶子节点。
4.4.3 扩容机制
当队列中的元素个数达到数组容量时,会调用grow()方法进行扩容:
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 如果旧容量小于64,则容量翻倍+2;否则,容量增加50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// 检查是否超过最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 复制元素到新数组
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 溢出
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
扩容规则是:如果当前容量小于64,则新容量为旧容量的两倍加2;否则,新容量为旧容量的1.5倍。如果新容量超过了最大数组容量(Integer.MAX_VALUE - 8),则使用Integer.MAX_VALUE。
4.5 优先级的定义方式
PriorityQueue中元素的优先级可以通过两种方式定义:
-
自然顺序:元素实现
Comparable接口,并重写compareTo()方法。class Student implements Comparable<Student> { private String name; private int score; public Student(String name, int score) { this.name = name; this.score = score; } // 按照分数升序排序(分数低的优先级高) @Override public int compareTo(Student o) { return Integer.compare(this.score, o.score); } @Override public String toString() { return name + "(" + score + ")"; } } public class PriorityQueueExample { public static void main(String[] args) { PriorityQueue<Student> pq = new PriorityQueue<>(); pq.add(new Student("张三", 80)); pq.add(new Student("李四", 60)); pq.add(new Student("王五", 90)); while (!pq.isEmpty()) { System.out.println(pq.poll()); // 依次输出:李四(60)、张三(80)、王五(90) } } } -
指定比较器:通过构造方法传入
Comparator对象,定义排序规则。class Student { private String name; private int score; public Student(String name, int score) { this.name = name; this.score = score; } public int getScore() { return score; } @Override public String toString() { return name + "(" + score + ")"; } } public class PriorityQueueExample { public static void main(String[] args) { // 按照分数降序排序(分数高的优先级高) PriorityQueue<Student> pq = new PriorityQueue<>( Comparator.comparingInt(Student::getScore).reversed() ); pq.add(new Student("张三", 80)); pq.add(new Student("李四", 60)); pq.add(new Student("王五", 90)); while (!pq.isEmpty()) { System.out.println(pq.poll()); // 依次输出:王五(90)、张三(80)、李四(60) } } }
4.6 PriorityQueue的使用场景
PriorityQueue适用于需要按照优先级处理元素的场景,例如:
- 任务调度:根据任务的优先级(如紧急程度)处理任务,优先级高的任务先执行。
- 事件驱动模拟:在模拟系统中,按照事件发生的时间顺序处理事件,时间早的事件先处理。
- Top K问题:从大量元素中找出前K个最大或最小的元素,使用
PriorityQueue可以高效实现。
以下是一个使用PriorityQueue解决Top K问题的示例:
import java.util.*;
public class TopKExample {
// 找出数组中前K个最大的元素
public static List<Integer> topK(int[] nums, int k) {
// 使用最小堆,堆的大小保持为k
PriorityQueue<Integer> pq = new PriorityQueue<>(k);
for (int num : nums) {
if (pq.size() < k) {
pq.offer(num);
} else if (num > pq.peek()) {
// 如果当前元素大于堆顶元素,替换堆顶元素
pq.poll();
pq.offer(num);
}
}
// 将堆中的元素转换为列表并反转(得到从大到小的顺序)
List<Integer> result = new ArrayList<>(pq);
Collections.reverse(result);
return result;
}
public static void main(String[] args) {
int[] nums = {3, 1, 4, 1, 5, 9, 2, 6};
int k = 3;
List<Integer> topK = topK(nums, k);
System.out.println("前" + k + "个最大的元素:" + topK); // 输出:前3个最大的元素:[9, 6, 5]
}
}
上述代码中,使用一个容量为k的最小堆来存储前k个最大的元素。对于每个元素,如果堆的大小小于k,则直接加入堆中;否则,如果当前元素大于堆顶元素(堆中最小的元素),则替换堆顶元素。最后,堆中的元素就是前k个最大的元素,反转后得到从大到小的顺序。
五、双端队列(Deque)及其实现类
5.1 Deque接口详解
Deque(Double-Ended Queue,双端队列)接口继承自Queue接口,它允许在队列的两端进行元素的插入和删除操作。因此,Deque既可以作为普通队列(FIFO)使用,也可以作为栈(LIFO,后进先出)使用。
5.1.1 Deque接口的定义
Deque接口的定义如下:
public interface Deque<E> extends Queue<E> {
// 向队头添加元素,失败抛异常
void addFirst(E e);
// 向队尾添加元素,失败抛异常
void addLast(E e);
// 向队头添加元素,失败返回false
boolean offerFirst(E e);
// 向队尾添加元素,失败返回false
boolean offerLast(E e);
// 移除并返回队头元素,失败抛异常
E removeFirst();
// 移除并返回队尾元素,失败抛异常
E removeLast();
// 移除并返回队头元素,失败返回null
E pollFirst();
// 移除并返回队尾元素,失败返回null
E pollLast();
// 返回但不移除队头元素,失败抛异常
E getFirst();
// 返回但不移除队尾元素,失败抛异常
E getLast();
// 返回但不移除队头元素,失败返回null
E peekFirst();
// 返回但不移除队尾元素,失败返回null
E peekLast();
// 移除第一次出现的指定元素
boolean removeFirstOccurrence(Object o);
// 移除最后一次出现的指定元素
boolean removeLastOccurrence(Object o);
// 以下方法与Queue接口的方法等价
boolean add(E e); // 等价于addLast(e)
boolean offer(E e); // 等价于offerLast(e)
E remove(); // 等价于removeFirst()
E poll(); // 等价于pollFirst()
E element(); // 等价于getFirst()
E peek(); // 等价于peekFirst()
// 以下方法与栈相关
void push(E e); // 等价于addFirst(e)
E pop(); // 等价于removeFirst()
// 其他继承自Collection的方法
// ...
}
从定义可以看出,Deque接口在Queue接口的基础上,增加了一系列用于操作队列两端的方法,以及与栈相关的push()和pop()方法。
5.1.2 Deque接口的方法分类
Deque接口的方法可以分为以下几类:
-
向两端添加元素:
addFirst(E e):向队头添加元素,失败抛异常。addLast(E e):向队尾添加元素,失败抛异常(与add(E e)等价)。offerFirst(E e):向队头添加元素,失败返回false。offerLast(E e):向队尾添加元素,失败返回false(与offer(E e)等价)。
-
从两端移除元素:
removeFirst():移除并返回队头元素,失败抛异常(与remove()等价)。removeLast():移除并返回队尾元素,失败抛异常。pollFirst():移除并返回队头元素,失败返回null(与poll()等价)。pollLast():移除并返回队尾元素,失败返回null。
-
查看两端元素:
getFirst():返回但不移除队头元素,失败抛异常(与element()等价)。getLast():返回但不移除队尾元素,失败抛异常。peekFirst():返回但不移除队头元素,失败返回null(与peek()等价)。peekLast():返回但不移除队尾元素,失败返回null。
-
移除指定元素:
removeFirstOccurrence(Object o):移除第一次出现的指定元素。removeLastOccurrence(Object o):移除最后一次出现的指定元素。
-
栈操作:
push(E e):向栈顶(队头)添加元素,等价于addFirst(e)。pop(E e):移除并返回栈顶(队头)元素,等价于removeFirst()。
5.2 ArrayDeque(双端队列实现)
前面已经介绍过ArrayDeque可以作为普通队列使用,实际上它是Deque接口的一个主要实现类,也是双端队列的推荐实现。
5.2.1 ArrayDeque作为双端队列的常用方法
除了作为普通队列使用的方法外,ArrayDeque作为双端队列使用时,还常用以下方法:
-
void addFirst(E e):向队头添加元素,元素为null时抛出NullPointerException。Deque<String> deque = new ArrayDeque<>(); deque.addFirst("a"); deque.addFirst("b"); System.out.println(deque); // 输出:[b, a] -
void addLast(E e):向队尾添加元素,与add(E e)等价。Deque<String> deque = new ArrayDeque<>(); deque.addLast("a"); deque.addLast("b"); System.out.println(deque); // 输出:[a, b] -
boolean offerFirst(E e):向队头添加元素,返回true,元素为null时抛出NullPointerException。Deque<String> deque = new ArrayDeque<>(); deque.offerFirst("a"); deque.offerFirst("b"); System.out.println(deque); // 输出:[b, a] -
boolean offerLast(E e):向队尾添加元素,返回true,与offer(E e)等价。Deque<String> deque = new ArrayDeque<>(); deque.offerLast("a"); deque.offerLast("b"); System.out.println(deque); // 输出:[a, b] -
E removeFirst():移除并返回队头元素,队列为空时抛出NoSuchElementException。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); deque.add("b"); System.out.println(deque.removeFirst()); // 输出:a -
E removeLast():移除并返回队尾元素,队列为空时抛出NoSuchElementException。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); deque.add("b"); System.out.println(deque.removeLast()); // 输出:b -
E pollFirst():移除并返回队头元素,队列为空时返回null。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); deque.add("b"); System.out.println(deque.pollFirst()); // 输出:a -
E pollLast():移除并返回队尾元素,队列为空时返回null。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); deque.add("b"); System.out.println(deque.pollLast()); // 输出:b -
E getFirst():返回但不移除队头元素,队列为空时抛出NoSuchElementException。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); System.out.println(deque.getFirst()); // 输出:a -
E getLast():返回但不移除队尾元素,队列为空时抛出NoSuchElementException。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); deque.add("b"); System.out.println(deque.getLast()); // 输出:b -
E peekFirst():返回但不移除队头元素,队列为空时返回null。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); System.out.println(deque.peekFirst()); // 输出:a -
E peekLast():返回但不移除队尾元素,队列为空时返回null。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); deque.add("b"); System.out.println(deque.peekLast()); // 输出:b -
boolean removeFirstOccurrence(Object o):移除第一次出现的指定元素。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); deque.add("b"); deque.add("a"); deque.removeFirstOccurrence("a"); System.out.println(deque); // 输出:[b, a] -
boolean removeLastOccurrence(Object o):移除最后一次出现的指定元素。Deque<String> deque = new ArrayDeque<>(); deque.add("a"); deque.add("b"); deque.add("a"); deque.removeLastOccurrence("a"); System.out.println(deque); // 输出:[a, b] -
void push(E e):向栈顶(队头)添加元素,等价于addFirst(e)。Deque<String> stack = new ArrayDeque<>(); stack.push("a"); stack.push("b"); System.out.println(stack); // 输出:[b, a] -
E pop():移除并返回栈顶(队头)元素,等价于removeFirst()。Deque<String> stack = new ArrayDeque<>(); stack.push("a"); stack.push("b"); System.out.println(stack.pop()); // 输出:b
5.2.2 ArrayDeque作为栈的使用
由于Deque接口提供了push()和pop()方法,ArrayDeque可以方便地作为栈使用,且性能通常优于java.util.Stack类(Stack是遗留类,继承自Vector,性能较差)。
以下是使用ArrayDeque作为栈的示例:
import java.util.ArrayDeque;
import java.util.Deque;
public class StackExample {
public static void main(String[] args) {
Deque<Integer> stack = new ArrayDeque<>();
// 入栈
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("栈中的元素:" + stack); // 输出:栈中的元素:[3, 2, 1]
// 查看栈顶元素
System.out.println("栈顶元素:" + stack.peek()); // 输出:栈顶元素:3
// 出栈
System.out.println("出栈元素:" + stack.pop()); // 输出:出栈元素:3
System.out.println("栈中的元素:" + stack); // 输出:栈中的元素:[2, 1]
// 再次出栈
System.out.println("出栈元素:" + stack.pop()); // 输出:出栈元素:2
System.out.println("栈中的元素:" + stack); // 输出:栈中的元素:[1]
}
}
5.3 LinkedList(双端队列实现)
LinkedList也实现了Deque接口,因此也可以作为双端队列使用。其作为双端队列的方法与ArrayDeque类似,但底层实现不同(链表 vs 数组)。
5.3.1 LinkedList与ArrayDeque的对比(作为双端队列)
| 特性 | ArrayDeque | LinkedList |
|---|---|---|
| 数据结构 | 动态数组 | 双向链表 |
| 内存占用 | 较低 | 较高(每个节点有额外引用) |
| 两端插入/删除效率 | 高(数组操作,除非需要扩容) | 高(链表节点操作) |
| 随机访问效率 | 高(O(1)) | 低(O(n)) |
| 是否允许null元素 | 不允许 | 允许 |
| 栈操作性能 | 高 | 较高 |
在大多数情况下,ArrayDeque作为双端队列或栈使用时性能优于LinkedList,因此推荐优先使用ArrayDeque。
六、阻塞队列(BlockingQueue)及其实现类
阻塞队列(BlockingQueue)是Java并发包(java.util.concurrent)中的重要组件,它继承自Queue接口,专门用于多线程环境。阻塞队列的特点是:当队列满时,入队操作会阻塞;当队列空时,出队操作会阻塞,直到队列有空间或有元素可用。
6.1 BlockingQueue接口详解
BlockingQueue接口扩展了Queue接口,增加了支持阻塞的入队和出队方法。
6.1.1 BlockingQueue接口的核心方法
BlockingQueue的核心方法可以分为以下几类:
-
阻塞式入队:
void put(E e):将元素添加到队列尾部,如果队列满则阻塞,直到队列有空间。如果元素为null,抛出NullPointerException;如果线程被中断,抛出InterruptedException。
-
超时阻塞式入队:
boolean offer(E e, long timeout, TimeUnit unit):将元素添加到队列尾部,如果队列满则等待指定的时间,如果在指定时间内队列有空间则添加成功并返回true,否则返回false。可能抛出NullPointerException、InterruptedException。
-
阻塞式出队:
E take():移除并返回队列头部元素,如果队列为空则阻塞,直到队列有元素。如果线程被中断,抛出InterruptedException。
-
超时阻塞式出队:
E poll(long timeout, TimeUnit unit):移除并返回队列头部元素,如果队列为空则等待指定的时间,如果在指定时间内队列有元素则返回该元素,否则返回null。可能抛出InterruptedException。
-
其他方法:
int remainingCapacity():返回队列剩余的容量(即还能添加多少元素)。boolean remove(Object o):移除队列中第一个出现的指定元素,返回true如果元素被移除。boolean contains(Object o):判断队列是否包含指定元素。int drainTo(Collection<? super E> c):将队列中的所有元素移到指定集合中,返回移动的元素个数。int drainTo(Collection<? super E> c, int maxElements):将队列中的最多maxElements个元素移到指定集合中,返回移动的元素个数。
BlockingQueue不允许添加null元素,否则会抛出NullPointerException。此外,BlockingQueue通常用于生产者-消费者模式,生产者线程向队列中添加元素,消费者线程从队列中取出元素。
6.1.2 BlockingQueue的实现类
BlockingQueue有多个实现类,各有不同的特性:
ArrayBlockingQueue:基于数组实现的有界阻塞队列。LinkedBlockingQueue:基于链表实现的可选有界阻塞队列(默认无界)。PriorityBlockingQueue:基于优先级堆实现的无界阻塞优先级队列。DelayQueue:基于优先级堆实现的无界阻塞延迟队列,元素只有在延迟时间到期后才能被取出。SynchronousQueue:不存储元素的阻塞队列,每个入队操作必须等待一个出队操作,反之亦然。LinkedTransferQueue:基于链表实现的无界阻塞队列,支持元素的直接传递。LinkedBlockingDeque:基于链表实现的双向阻塞队列。
接下来,我们将详细讲解这些实现类。
6.2 ArrayBlockingQueue
ArrayBlockingQueue是基于数组实现的有界阻塞队列,它的容量在创建时确定,之后不能更改。ArrayBlockingQueue内部使用重入锁(ReentrantLock)和条件变量(Condition)来保证线程安全。
6.2.1 ArrayBlockingQueue的特性
- 数据结构:数组,容量固定。
- 容量:有界,创建时指定,不可更改。
- 线程安全性:线程安全,内部使用单一锁保证。
- 元素排序:按照FIFO原则排序。
- 公平性:可以指定是否为公平队列。公平队列保证线程按照等待顺序获取锁,而非公平队列则不保证,后者吞吐量通常更高。
6.2.2 ArrayBlockingQueue的构造方法
-
public ArrayBlockingQueue(int capacity):创建一个指定容量的ArrayBlockingQueue,默认是非公平的。BlockingQueue<String> queue = new ArrayBlockingQueue<>(10); -
public ArrayBlockingQueue(int capacity, boolean fair):创建一个指定容量和公平性的ArrayBlockingQueue。fair为true表示公平队列,false表示非公平队列。BlockingQueue<String> queue = new ArrayBlockingQueue<>(10, true); // 公平队列 -
public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c):创建一个指定容量、公平性,并包含指定集合元素的ArrayBlockingQueue。集合中的元素会按照迭代器顺序添加到队列中,如果集合元素个数超过容量,会抛出IllegalArgumentException。List<String> list = Arrays.asList("a", "b", "c"); BlockingQueue<String> queue = new ArrayBlockingQueue<>(10, false, list);
6.2.3 ArrayBlockingQueue的常用方法
除了BlockingQueue接口定义的方法外,ArrayBlockingQueue还继承了Queue和Collection接口的方法,以下是常用方法的示例:
-
void put(E e):BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); try { queue.put("a"); queue.put("b"); // 队列已满,以下代码会阻塞 // queue.put("c"); } catch (InterruptedException e) { e.printStackTrace(); } -
boolean offer(E e, long timeout, TimeUnit unit):BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); try { queue.offer("a"); queue.offer("b"); // 队列已满,等待1秒 boolean result = queue.offer("c", 1, TimeUnit.SECONDS); System.out.println(result); // 输出:false(1秒内队列仍满) } catch (InterruptedException e) { e.printStackTrace(); } -
E take():BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); new Thread(() -> { try { Thread.sleep(1000); // 休眠1秒后添加元素 queue.put("a"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); try { // 队列为空,会阻塞直到有元素 String element = queue.take(); System.out.println(element); // 输出:a } catch (InterruptedException e) { e.printStackTrace(); } -
E poll(long timeout, TimeUnit unit):BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); try { // 队列为空,等待1秒 String element = queue.poll(1, TimeUnit.SECONDS); System.out.println(element); // 输出:null(1秒内队列仍空) } catch (InterruptedException e) { e.printStackTrace(); } -
int remainingCapacity():BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); queue.offer("a"); System.out.println(queue.remainingCapacity()); // 输出:1
6.2.4 ArrayBlockingQueue的底层实现原理
ArrayBlockingQueue的核心字段包括:
final Object[] items:存储元素的数组。int takeIndex:下一个出队元素的索引。int putIndex:下一个入队元素的索引。int count:队列中的元素个数。final ReentrantLock lock:保证线程安全的重入锁。private final Condition notEmpty:当队列空时,等待的条件变量(供消费者使用)。private final Condition notFull:当队列满时,等待的条件变量(供生产者使用)。
put(E e)方法的实现如下:
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();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0; // 循环数组
count++;
notEmpty.signal(); // 唤醒等待的消费者线程
}
take()方法的实现如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); // 队列空,等待
return dequeue(); // 出队
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0; // 循环数组
count--;
notFull.signal(); // 唤醒等待的生产者线程
return x;
}
从实现可以看出,ArrayBlockingQueue使用单一锁来控制所有访问,因此生产者和消费者线程会竞争同一把锁,在高并发场景下可能会有一定的性能瓶颈。
6.2.5 ArrayBlockingQueue的使用场景
ArrayBlockingQueue适用于以下场景:
- 已知队列最大容量的多线程场景。
- 生产者和消费者之间需要同步的场景,如线程池中的任务队列(部分线程池实现使用了类似的原理)。
以下是一个使用ArrayBlockingQueue实现生产者-消费者模式的示例:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumer {
private static final int QUEUE_CAPACITY = 5;
private static final int NUM_ITEMS = 10;
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < NUM_ITEMS; i++) {
System.out.println("生产者生产:" + i);
queue.put(i);
Thread.sleep(100); // 模拟生产耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < NUM_ITEMS; i++) {
int item = queue.take();
System.out.println("消费者消费:" + item);
Thread.sleep(200); // 模拟消费耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
在这个示例中,生产者线程不断生产元素并放入队列,消费者线程不断从队列中取出元素消费。由于消费者消费速度慢于生产者生产速度,当队列满时,生产者会阻塞等待,直到消费者消费了元素腾出空间。
6.3 LinkedBlockingQueue
LinkedBlockingQueue是基于链表实现的阻塞队列,它可以是有界的也可以是无界的(默认无界)。与ArrayBlockingQueue相比,LinkedBlockingQueue在高并发场景下通常具有更高的吞吐量,因为它使用两把锁(分别控制入队和出队),生产者和消费者可以并行操作。
6.3.1 LinkedBlockingQueue的特性
- 数据结构:单向链表(节点包含元素和下一个节点的引用)。
- 容量:可选有界,默认值为
Integer.MAX_VALUE(无界)。 - 线程安全性:线程安全,内部使用两把锁(
takeLock和putLock)分别控制入队和出队操作。 - 元素排序:按照FIFO原则排序。
- 内存占用:每个元素需要额外的节点对象,内存占用高于
ArrayBlockingQueue。
6.3.2 LinkedBlockingQueue的构造方法
-
public LinkedBlockingQueue():创建一个默认容量为Integer.MAX_VALUE的无界LinkedBlockingQueue。BlockingQueue<String> queue = new LinkedBlockingQueue<>(); -
public LinkedBlockingQueue(int capacity):创建一个指定容量的有界LinkedBlockingQueue。capacity必须大于0,否则抛出IllegalArgumentException。BlockingQueue<String> queue = new LinkedBlockingQueue<>(10); -
public LinkedBlockingQueue(Collection<? extends E> c):创建一个包含指定集合元素的LinkedBlockingQueue,容量为集合元素个数与Integer.MAX_VALUE中的较小值。List<String> list = Arrays.asList("a", "b", "c"); BlockingQueue<String> queue = new LinkedBlockingQueue<>(list);
6.3.3 LinkedBlockingQueue的常用方法
LinkedBlockingQueue的常用方法与ArrayBlockingQueue类似,包括put()、offer()、take()、poll()等,以下是一些示例:
-
void put(E e):BlockingQueue<String> queue = new LinkedBlockingQueue<>(2); try { queue.put("a"); queue.put("b"); // 队列已满,以下代码会阻塞 // queue.put("c"); } catch (InterruptedException e) { e.printStackTrace(); } -
E take():BlockingQueue<String> queue = new LinkedBlockingQueue<>(2); new Thread(() -> { try { Thread.sleep(1000); queue.put("a"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); try { String element = queue.take(); System.out.println(element); // 输出:a } catch (InterruptedException e) { e.printStackTrace(); }
6.3.4 LinkedBlockingQueue的底层实现原理
LinkedBlockingQueue的核心字段包括:
private final int capacity:队列的容量。private final AtomicInteger count:队列中的元素个数(使用原子类保证线程安全)。transient Node<E> head:头节点(头节点不存储元素,其下一个节点是第一个元素)。transient Node<E> last:尾节点。private final ReentrantLock takeLock:控制出队操作的锁。private final Condition notEmpty:与takeLock关联的条件变量,用于等待元素。private final ReentrantLock putLock:控制入队操作的锁。private final Condition notFull:与putLock关联的条件变量,用于等待空间。
节点的定义如下:
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
put(E e)方法的实现如下:
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 队列满,等待
while (count.get() == capacity) {
notFull.await();
}
enqueue(node); // 入队
c = count.getAndIncrement();
// 如果还有空间,唤醒其他生产者
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果队列从空变为非空,唤醒消费者
if (c == 0)
signalNotEmpty();
}
private void enqueue(Node<E> node) {
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
take()方法的实现如下:
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 队列空,等待
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue(); // 出队
c = count.getAndDecrement();
// 如果还有元素,唤醒其他消费者
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列从满变为非满,唤醒生产者
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // 帮助GC
head = first;
E x = first.item;
first.item = null;
return x;
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
由于LinkedBlockingQueue使用两把锁分离了入队和出队操作,生产者和消费者可以同时操作队列,因此在高并发场景下通常比ArrayBlockingQueue具有更高的吞吐量。
6.3.5 LinkedBlockingQueue的使用场景
LinkedBlockingQueue适用于以下场景:
- 队列容量较大或不确定的多线程场景。
- 生产者和消费者并发程度较高的场景,如日志收集、消息传递等。
LinkedBlockingQueue是Java线程池(如ThreadPoolExecutor)中常用的任务队列之一。
6.4 PriorityBlockingQueue
PriorityBlockingQueue是基于优先级堆实现的无界阻塞优先级队列,它继承自AbstractQueue并实现了BlockingQueue接口。与PriorityQueue类似,PriorityBlockingQueue中的元素按照优先级排序,但它是线程安全的,适用于多线程环境。
6.4.1 PriorityBlockingQueue的特性
- 数据结构:优先级堆(数组实现的二叉堆)。
- 容量:无界,初始容量为11,当元素数量超过容量时会自动扩容。
- 线程安全性:线程安全,内部使用重入锁(
ReentrantLock)保证。 - 元素排序:默认按照元素的自然顺序排序,也可以通过构造方法指定
Comparator。 - 阻塞特性:当队列为空时,出队操作(
take())会阻塞,入队操作永远不会阻塞(因为是无界队列)。
6.4.2 PriorityBlockingQueue的构造方法
-
public PriorityBlockingQueue():创建一个初始容量为11,按照元素自然顺序排序的PriorityBlockingQueue。BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(); -
public PriorityBlockingQueue(int initialCapacity):创建一个指定初始容量,按照元素自然顺序排序的PriorityBlockingQueue。initialCapacity必须大于0,否则抛出IllegalArgumentException。BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(20); -
public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator):创建一个指定初始容量和比较器的PriorityBlockingQueue。BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(20, Collections.reverseOrder()); -
public PriorityBlockingQueue(Collection<? extends E> c):创建一个包含指定集合元素的PriorityBlockingQueue。如果集合是SortedSet或PriorityQueue,则按照其排序规则排序;否则,按照元素的自然顺序排序。List<Integer> list = Arrays.asList(3, 1, 2); BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(list);
6.4.3 PriorityBlockingQueue的常用方法
PriorityBlockingQueue的常用方法包括put()、offer()、take()、poll()等,其中入队方法不会阻塞(因为是无界队列),出队方法在队列为空时会阻塞。
-
void put(E e):由于队列无界,此方法不会阻塞,等价于offer(e)。BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(); try { queue.put(3); queue.put(1); queue.put(2); } catch (InterruptedException e) { e.printStackTrace(); } -
E take():移除并返回优先级最高的元素,队列为空时阻塞。BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(); new Thread(() -> { try { Thread.sleep(1000); queue.put(3); queue.put(1); queue.put(2); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); try { System.out.println(queue.take()); // 输出:1(优先级最高) System.out.println(queue.take()); // 输出:2 System.out.println(queue.take()); // 输出:3 } catch (InterruptedException e) { e.printStackTrace(); }
6.4.4 PriorityBlockingQueue的底层实现原理
PriorityBlockingQueue的核心字段包括:
private transient Object[] queue:存储元素的数组(堆)。private transient int size:元素个数。private transient Comparator<? super E> comparator:比较器。private final ReentrantLock lock:重入锁。private final Condition notEmpty:条件变量,用于等待元素。private transient volatile int allocationSpinLock:用于扩容时的自旋锁。
put(E e)方法的实现如下(实际上调用了offer(e)):
public void put(E e) {
offer(e); // never need to block
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
// 如果元素个数达到当前容量,扩容
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
take()方法的实现如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null )
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
private E dequeue() {
int n = size - 1;
if (n < 0)
return null;
else {
Object[] array = queue;
E result = (E) array[0];
E x = (E) array[n];
array[n] = null;
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
PriorityBlockingQueue的扩容机制与PriorityQueue类似,但使用了自旋锁来保证扩容的线程安全:
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 释放主锁,允许其他线程操作
Object[] newArray = null;
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) {
try {
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
allocationSpinLock = 0;
}
}
// 如果其他线程正在扩容,当前线程让出CPU
if (newArray == null) // back off if another thread is allocating
Thread.yield();
lock.lock();
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
6.4.5 PriorityBlockingQueue的使用场景
PriorityBlockingQueue适用于多线程环境下需要按照优先级处理元素的场景,例如:
- 多线程任务调度,优先级高的任务先执行。
- 多线程环境下的Top K问题处理。
以下是一个使用PriorityBlockingQueue实现多线程任务调度的示例:
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
class Task implements Comparable<Task> {
private String name;
private int priority; // 优先级,值越小优先级越高
public Task(String name, int priority) {
this.name = name;
this.priority = priority;
}
public void execute() {
System.out.println("执行任务:" + name + ",优先级:" + priority);
}
@Override
public int compareTo(Task o) {
return Integer.compare(this.priority, o.priority);
}
}
public class PriorityTaskScheduler {
private PriorityBlockingQueue<Task> taskQueue = new PriorityBlockingQueue<>();
private volatile boolean isRunning = true;
public void start() {
new Thread(() -> {
while (isRunning) {
try {
Task task = taskQueue.take(); // 阻塞等待任务
task.execute();
TimeUnit.MILLISECONDS.sleep(500); // 模拟执行耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
isRunning = false;
}
}
}).start();
}
public void addTask(Task task) {
taskQueue.offer(task);
}
public void stop() {
isRunning = false;
}
public static void main(String[] args) throws InterruptedException {
PriorityTaskScheduler scheduler = new PriorityTaskScheduler();
scheduler.start();
// 添加任务
scheduler.addTask(new Task("任务1", 3));
scheduler.addTask(new Task("任务2", 1));
scheduler.addTask(new Task("任务3", 2));
// 等待所有任务执行完成
TimeUnit.SECONDS.sleep(3);
scheduler.stop();
}
}
运行上述代码,输出结果为:
执行任务:任务2,优先级:1
执行任务:任务3,优先级:2
执行任务:任务1,优先级:3
可以看到,任务按照优先级从高到低的顺序执行,符合PriorityBlockingQueue的特性。
(由于篇幅限制,其他阻塞队列实现类的详细讲解在此省略,如需了解可进一步扩展)
七、并发队列(ConcurrentQueue)及其实现类
并发队列是指支持多线程并发操作的队列,它们通常通过非阻塞算法(如CAS操作)来保证线程安全,避免了锁竞争带来的性能开销,因此在高并发场景下具有更好的性能。
7.1 ConcurrentQueue接口
ConcurrentQueue接口是Java并发包中所有并发队列的父接口,它继承自Queue接口,定义了并发队列的基本特性,但没有添加新的方法:
public interface ConcurrentQueue<E> extends Queue<E> {
}
ConcurrentQueue的主要实现类有ConcurrentLinkedQueue和ConcurrentLinkedDeque。
7.2 ConcurrentLinkedQueue
ConcurrentLinkedQueue是基于链表实现的无界并发队列,它使用CAS(Compare-And-Swap)非阻塞算法来保证线程安全,适用于高并发场景。
7.2.1 ConcurrentLinkedQueue的特性
- 数据结构:单向链表(节点包含元素、下一个节点的引用等)。
- 容量:无界,理论上可以无限添加元素(受限于内存)。
- 线程安全性:线程安全,使用CAS操作保证,无锁机制。
- 元素排序:按照FIFO原则排序。
- 元素允许:不允许
null元素。 - 性能:高并发场景下性能优异,避免了锁竞争。
7.2.2 ConcurrentLinkedQueue的构造方法
-
public ConcurrentLinkedQueue():创建一个空的ConcurrentLinkedQueue。ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); -
public ConcurrentLinkedQueue(Collection<? extends E> c):创建一个包含指定集合元素的ConcurrentLinkedQueue,元素顺序与集合的迭代器返回顺序一致。List<String> list = Arrays.asList("a", "b", "c"); ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(list);
7.2.3 ConcurrentLinkedQueue的常用方法
ConcurrentLinkedQueue实现了Queue接口的所有方法,常用方法包括:
-
boolean add(E e):向队尾添加元素,由于是无界队列,始终返回true,元素为null时抛出NullPointerException。ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); queue.add("a"); queue.add("b"); -
boolean offer(E e):与add()方法功能相同,向队尾添加元素,返回true。ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); queue.offer("a"); queue.offer("b"); -
E poll():移除并返回队头元素,队列为空时返回null。ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); queue.add("a"); System.out.println(queue.poll()); // 输出:a -
E peek():返回但不移除队头元素,队列为空时返回null。ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); queue.add("a"); System.out.println(queue.peek()); // 输出:a -
boolean isEmpty():判断队列是否为空。ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); System.out.println(queue.isEmpty()); // 输出:true -
int size():返回队列中的元素个数。注意,在并发环境下,size()方法的结果可能不准确,因为在计算过程中队列可能被其他线程修改。ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); queue.add("a"); queue.add("b"); System.out.println(queue.size()); // 输出:2
7.2.4 ConcurrentLinkedQueue的底层实现原理
ConcurrentLinkedQueue的核心字段包括:
private transient volatile Node<E> head:头节点。private transient volatile Node<E> tail:尾节点。
节点的定义如下(简化版):
private static class Node<E> {
volatile E item;
volatile Node<E> next;
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
// 静态字段偏移量,用于UNSAFE操作
private static final sun.misc.Unsafe UNSAFE;
private static final long itemOffset;
private static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = Node.class;
itemOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
ConcurrentLinkedQueue的入队(offer())和出队(poll())操作均通过CAS实现,避免了锁的使用。
offer(E e)方法的实现逻辑如下:
- 检查元素是否为
null,如果是则抛出异常。 - 创建新节点。
- 从尾节点开始,通过CAS操作将新节点添加到队列尾部,直到成功。
- 必要时更新尾节点(尾节点可能滞后于实际的最后一个节点)。
poll()方法的实现逻辑如下:
- 从头部开始,循环查找第一个有元素的节点。
- 通过CAS操作将该节点的元素设为
null(标记为删除)。 - 必要时更新头节点(头节点可能是已删除的节点)。
- 返回被删除节点的元素。
由于使用了CAS操作,ConcurrentLinkedQueue在多线程环境下可以高效地并发操作,不会出现锁竞争导致的线程阻塞。
7.2.5 ConcurrentLinkedQueue的使用场景
ConcurrentLinkedQueue适用于高并发场景下的队列操作,例如:
- 多线程日志收集,多个线程同时向队列中添加日志,一个线程负责处理日志。
- 多生产者-多消费者模式,多个生产者向队列中添加任务,多个消费者从队列中取出任务执行。
以下是一个多生产者-多消费者的示例:
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
public class ConcurrentQueueExample {
private static ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
private static final int PRODUCER_COUNT = 2;
private static final int CONSUMER_COUNT = 2;
private static final int ITEM_COUNT = 5;
public static void main(String[] args) throws InterruptedException {
// 启动生产者线程
for (int i = 0; i < PRODUCER_COUNT; i++) {
final int producerId = i;
new Thread(() -> {
for (int j = 0; j < ITEM_COUNT; j++) {
int item = producerId * ITEM_COUNT + j;
queue.offer(item);
System.out.println("生产者" + producerId + "生产:" + item);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
// 启动消费者线程
for (int i = 0; i < CONSUMER_COUNT; i++) {
final int consumerId = i;
new Thread(() -> {
int count = 0;
while (count < ITEM_COUNT * PRODUCER_COUNT / CONSUMER_COUNT) {
Integer item = queue.poll();
if (item != null) {
System.out.println("消费者" + consumerId + "消费:" + item);
count++;
try {
TimeUnit.MILLISECONDS.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
// 等待所有线程完成
TimeUnit.SECONDS.sleep(5);
}
}
在这个示例中,2个生产者线程各生产5个元素,2个消费者线程共同消费这些元素。由于使用了ConcurrentLinkedQueue,多线程可以安全地并发操作队列。
八、队列的选择与使用建议
在实际开发中,选择合适的队列实现类对于程序的性能和正确性至关重要。以下是一些选择队列的建议:
-
单线程环境:
- 如果需要普通队列(FIFO),优先选择
ArrayDeque,它的性能优于LinkedList。 - 如果需要双端队列或栈,优先选择
ArrayDeque。 - 如果需要优先级队列,选择
PriorityQueue。
- 如果需要普通队列(FIFO),优先选择
-
多线程环境:
- 如果需要阻塞队列(生产者-消费者模式):
- 队列容量固定:选择
ArrayBlockingQueue。 - 队列容量较大或不确定:选择
LinkedBlockingQueue。 - 需要按照优先级处理元素:选择
PriorityBlockingQueue。 - 需要元素延迟处理:选择
DelayQueue。 - 需要同步移交元素:选择
SynchronousQueue。
- 队列容量固定:选择
- 如果不需要阻塞,只需要线程安全的并发队列:选择
ConcurrentLinkedQueue(普通队列)或ConcurrentLinkedDeque(双端队列)。
- 如果需要阻塞队列(生产者-消费者模式):
-
特殊需求:
- 需要延迟执行任务:使用
DelayQueue。 - 需要高效的栈操作:使用
- 需要延迟执行任务:使用

1859

被折叠的 条评论
为什么被折叠?



