1. Queue集合
1.1 Queue集合概述
JDK源码对Queue集合是这么解释的,大家看看。
A collection designed for holding elements prior to processing.
专为在处理之前保存元素而设计的集合。
胡广是这么理解的,List集合用于存储常用元素、Map集合用于存储具有映射关系的元素、Set集合用于存储唯一性的元素。Queue集合呢?所有的数据结构都是为了解决业务问题而生,而Queue集合这种数据结构能够存储具有先后时间关系的元素,很适用于在业务高峰期,需要缓存当前任务的业务场景。像Kafka、RabbitMQ、RocketMQ都是队列任务这种思想。
Queue集合底层接口提供的方法很简单,一共有 6 个。
// 添加元素。没有可用元素则简单返回false
boolean offer(E e);
// 添加元素。没有可用元素则抛出IllegalStateException
boolean add(E e);
// 移除队列的头部元素。如果此队列为空则返回null 。
E poll();
// 移除队列的头部元素。该方法和poll不同之处在于,如果此队列为空,则抛出异常。
E remove();
// 检索但不移除此队列的头部。如果此队列为空,则返回null 。
E peek();
// 检索但不移除此队列的头部。该方法和peek唯一不同之处在于,如果此队列为空,则抛出异常
E element();
Queue集合常用的实现类如下,我会一一讲来。包括双端队列的两个实现类:LinkedList、ArrayDeque,优先级队列:PriorityQueue,线程安全相关的Queue实现类:LinkedBlockingQueue、ArrayBlockingQueue、ConcurrentLinkedQueue。
1.2 双端队列
双端队列是Queue集合的一个子接口,顾名思义相比普通队列来说,双端队列可以往前、也可以往后顺序插入元素。比如我下面给出一段队列的初始化。
Queue<Integer> queue = new LinkedList<>();
Deque<Integer> deque = new LinkedList<>();
queue.add(1);
deque.addLast(1);
deque.addFirst(1);
同样是new LinkedList<>()
来实例化队列,如果使用双端队列Deque接口,那这个队列就可以使用addFirst
、addLast
等双端队列特有的方法。
有朋友就会问:那ArrayQueue呢?这两者都是双端队列Deque的底层实现,但底层数据结构不同,LinkedList底层数据结构是一个双向链表,看看它有前指针next
、后指针prev
。
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;
}
}
而ArrayDeque底层数据结构是一个Object类型的数组。
transient Object[] elements;
为什么要这么设计呢?其实这两种不同的设计就可以高效适用于不同的业务场景。双向链表实现的Deque随机查找的性能差,但插入、删除元素时性能非常出色,适用于修改操作更频繁的业务场景。
而数组实现的Deque,也就是ArrayDeque,它的插入、删除操作性能不如前者,但随机查找的性能非常出色,更适用于查询操作更频繁的业务场景。
1.3 优先级队列
优先级队列的实现类叫PriorityQueue,PriorityQueue虽然属于队列的一份子,不过它违背了队列最基本的原则:FIFO先进先出原则。它背叛了组织!
PriorityQueue的特性是它并不按常规队列一样顺序存储,而是根据元素的自然顺序进行排序,使用出队列的方法也是输出当前优先级最高的元素。例如以下代码输出的一样。
public static void main(String[] args) {
Queue<Integer> queue = new PriorityQueue<>();
queue.offer(6);
queue.offer(1);
queue.offer(3);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
# 执行结果
1
3
6
但如果我们直接打印PriorityQueue的所有元素,发现他其实并不是按元素的自然顺序进行存储。
public static void main(String[] args) {
Queue<Integer> queue = new PriorityQueue<>();
queue.offer(6);
queue.offer(1);
queue.offer(3);
System.out.println(queue);
}
# 执行结果
[1, 6, 3]
why?其实PriorityQueue底层数据结构是一个平衡二叉堆:transient Object[] queue
,如果你直接打印,打印的是堆里面的存储元素。
对于PriorityQueue来说,它只保证你使用poll()
操作时,返回的是队列中最小、或最大的元素。
1.4 阻塞队列
JDK提供的阻塞队列接口为BlockingQueue,胡广先说说BlockingQueue的子类实现之一:ArrayBlockingQueue。
阻塞队列的特别之处在于当生产者线程会往队列放入元素时,如果队列已满,则生产者线程会进入阻塞状态;而当消费者线程往队列取出元素时,如果队列空了,则消费者线程会进入阻塞状态。
但队列的状态从满变为不满,消费者线程会唤醒生产者线程继续生产;队列的状态从空变为不空,生产者线程会唤醒消费者线程继续消费。
所以ArrayBlockingQueue很适合用于实现生产者、消费者场景。大家看看这个Demo。
public class Test {
public static void main(String[] args) {
// 创建一个容量为 3 的ArrayBlockingQueue
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
// 创建并启动生产者线程
Thread producerThread = new Thread(new Producer(queue));
Thread consumerThread = new Thread(new Consumer(queue));
producerThread.start();
consumerThread.start();
}
}
// 生产者类
class Producer implements Runnable {
private BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
for (int i = 1; i <= 6; i++) {
System.out.println("生产者生产了: " + i);
queue.put(i);
Thread.sleep(150); // 模拟生产过程中的延迟
}
queue.put(-1); // 使用特殊值表示结束生产
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 消费者类
class Consumer implements Runnable {
private BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
Integer item = queue.take();
if (item == -1) {
break; // 遇到特殊值,退出循环
}
System.out.println("消费者消费了: " + item);
Thread.sleep(100); // 模拟消费过程中的延迟
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
# 执行结果
生产者生产了: 1
消费者消费了: 1
生产者生产了: 2
消费者消费了: 2
生产者生产了: 3
消费者消费了: 3
生产者生产了: 4
消费者消费了: 4
生产者生产了: 5
消费者消费了: 5
生产者生产了: 6
消费者消费了: 6
LinkedBlockingQueue也是阻塞队列的实现之一,不过它和上面的ArrayBlockingQueue区别在于底层数据结构是由双向链表
进行实现。
// LinkedBlockingQueue源码双向链表
transient Node<E> head;
private transient Node<E> last;
2. AI生成的Queue面试题
1. 什么是 Java 中的 Queue 接口?有哪些实现类?
答: Java 中的 Queue
是一个接口,继承自 Collection
接口。它表示一个先进先出(FIFO)的数据结构,即最先进入队列的元素会最先被处理。常用的实现类有:
-
LinkedList
-
PriorityQueue
-
ArrayBlockingQueue
-
LinkedBlockingQueue
-
ConcurrentLinkedQueue
-
DelayQueue
-
SynchronousQueue
每个实现类有不同的特点,比如 PriorityQueue 按优先级排序,ArrayBlockingQueue 是有界队列,ConcurrentLinkedQueue 是线程安全的无界队列。
2. Queue 和 Deque 的区别是什么?
答: Queue
是单向队列,遵循先进先出(FIFO)原则,只允许从队尾添加元素,从队首移除元素。而 Deque 是双端队列,可以从队列的两端添加和移除元素,支持先进先出(FIFO)和后进先出(LIFO)的操作。
3. Java 中的 Queue 接口有哪些常用方法?
答: Queue
接口的常用方法有:
-
add(E e)
:将元素插入队列,如果队列满则抛出异常。 -
offer(E e)
:将元素插入队列,返回true
或false
。 -
remove()
:移除并返回队列头部的元素,队列为空则抛出异常。 -
poll()
:移除并返回队列头部的元素,队列为空返回null
。 -
element()
:返回队列头部的元素但不移除,队列为空则抛出异常。 -
peek()
:返回队列头部的元素但不移除,队列为空返回null
。
4. Java 中的 PriorityQueue 是如何工作的?
答: PriorityQueue
是一个基于优先级的队列,元素按自然顺序或提供的比较器顺序进行排序。它不是 FIFO 队列,元素的顺序由优先级决定,每次移除或查看的元素是队列中优先级最高的元素。PriorityQueue
底层基于最小堆实现,peek()
或 poll()
操作返回堆顶的元素。
5. 如何实现一个线程安全的 Queue?
答: Java 提供了多种线程安全的队列实现,例如:
-
ConcurrentLinkedQueue:基于无锁算法的非阻塞队列,适合高并发环境。
-
LinkedBlockingQueue:基于链表的阻塞队列,支持可选的容量限制,适合生产者-消费者模型。
-
ArrayBlockingQueue:基于数组的有界阻塞队列,适合在多线程环境中限制队列的容量。
这些类通过内部的锁机制或无锁算法,确保在多线程环境下的并发安全。
6. 阻塞队列是什么?Java 中有哪些阻塞队列?
答: 阻塞队列(BlockingQueue)是支持阻塞插入和移除操作的队列。插入操作在队列满时阻塞,移除操作在队列为空时阻塞。常见的阻塞队列有:
-
ArrayBlockingQueue
-
LinkedBlockingQueue
-
PriorityBlockingQueue
-
SynchronousQueue
-
DelayQueue
它们在多线程环境中,尤其是生产者-消费者模型中,广泛使用。
7. 什么是 DelayQueue?它的使用场景是什么?
答: DelayQueue 是一个特殊的阻塞队列,其中的元素只有在指定的延迟时间到期后,才能从队列中取出。该队列适用于需要延迟处理任务的场景,例如:
-
实现任务调度系统
-
缓存失效机制
-
延迟消息队列
DelayQueue
只接受实现了 Delayed
接口的元素,compareTo()
方法决定元素的排序。
8. 如何使用 BlockingQueue 实现生产者-消费者模型?
答: BlockingQueue
提供了线程安全的阻塞操作,非常适合生产者-消费者模型。生产者使用 put()
方法将数据放入队列,消费者使用 take()
方法从队列中取出数据。当队列满时,生产者会被阻塞;当队列为空时,消费者会被阻塞。典型的实现代码如下:
java复制代码BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
// 生产者
new Thread(() -> {
try {
queue.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 消费者
new Thread(() -> {
try {
Integer data = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
9. ConcurrentLinkedQueue 是如何实现线程安全的?
答: ConcurrentLinkedQueue 是一种无界非阻塞队列,它使用 无锁算法 实现线程安全。具体来说,它使用 CAS(Compare-And-Swap) 操作来确保队列中元素的原子性插入和移除操作。由于它是无锁的,性能相比使用锁的队列更高,特别适合高并发的场景。
10. SynchronousQueue 的特点是什么?有什么应用场景?
答: SynchronousQueue 是一种没有内部容量的阻塞队列,每个插入操作必须等待相应的移除操作发生,反之亦然。也就是说,生产者线程必须等待消费者线程取走元素,生产者和消费者必须一一配对。常见的应用场景包括:
-
线程池中的任务调度
-
在高效传递单个任务时,作为线程之间的“交换点”
11. PriorityBlockingQueue 和 PriorityQueue 有什么不同?
答: PriorityBlockingQueue
是 PriorityQueue
的线程安全版本,它是一个阻塞队列,支持元素按优先级排序。区别在于:
-
PriorityQueue 不是线程安全的,不支持阻塞操作。
-
PriorityBlockingQueue 是线程安全的,支持阻塞插入和移除操作,适合在多线程环境中使用。
12. 队列的大小是如何管理的?哪些队列是有界队列?
答: 在 Java 中,有些队列是有界的,即可以设置一个固定的容量,当队列达到该容量时,插入操作会被阻塞。常见的有界队列包括:
-
ArrayBlockingQueue
-
LinkedBlockingQueue(可以设置容量限制)
-
PriorityBlockingQueue(可以设置初始容量,但它是无界的)
通过设置队列大小,避免过度消耗内存或资源,适合资源受限的应用场景。
13. 如何实现自定义优先级的 PriorityQueue?
答: PriorityQueue
支持通过 Comparator
来自定义元素的优先级排序。可以在构造 PriorityQueue
时传入一个自定义的比较器。例如:
java复制代码PriorityQueue<Task> queue = new PriorityQueue<>((t1, t2) -> {
return t1.getPriority() - t2.getPriority();
});
这里 Task
是一个任务类,通过比较 Task
对象的优先级字段,队列中会按任务优先级从低到高排序。
14. 如何避免 Queue 中的死锁问题?
答: 在多线程环境中使用 BlockingQueue
时可能会发生死锁。为了避免死锁,可以:
-
使用有界队列:设置队列的最大容量,防止队列无限增长导致内存耗尽。
-
超时机制:使用带超时的插入和移除操作,如
offer(E e, long timeout, TimeUnit unit)
和poll(long timeout, TimeUnit unit)
,避免永远阻塞。 -
合理设计线程池:确保生产者和消费者的线程数相匹配,避免生产者或消费者线程饥饿。
15. 在什么场景下会使用 LinkedBlockingQueue 而不是 ArrayBlockingQueue?
答: LinkedBlockingQueue
和 ArrayBlockingQueue
都是阻塞队列,但它们有以下不同:
-
LinkedBlockingQueue 基于链表实现,可以是有界或无界,适合对队列大小不确定的场景。
-
ArrayBlockingQueue 基于数组实现,是有界队列,适合需要限制内存使用的场景。
如果队列大小不固定或希望动态扩展,使用 LinkedBlockingQueue
更为合适;如果需要对队列大小进行限制,避免内存溢出,ArrayBlockingQueue
更合适。
结束啦,希望大家能有所成!!!
你好,我是胡广。 致力于为帮助兄弟们的学习方式、面试困难、入职经验少走弯路而写博客 🌹🌹🌹 坚持每天两篇高质量文章输出,加油!!!🤩
如果本篇文章帮到了你 不妨点个赞吧~ 我会很高兴的 😄 (^ ~ ^) 。想看更多 那就点个关注 吧 我会尽力带来有趣的内容 。
😎感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以 给我留言咨询,希望帮助更多的人
更多专栏:
📊 Java设计模式宝典:从入门到精通(持续更新)📡 未完待续。。。
🎯 未完待续。。。
🔍 未完待续。。。
感谢订阅专栏 三连文章