第1章:Queue与Deque基础
1.1 Queue接口基础
1.1.1 Queue接口定义
核心特点
Queue是Java集合框架中用于存储元素的队列接口,它遵循**FIFO(First In First Out,先进先出)**原则。
public interface Queue<E> extends Collection<E> {
// 添加元素
boolean add(E e); // 添加元素,失败抛出异常
boolean offer(E e); // 添加元素,失败返回false
// 移除元素
E remove(); // 移除并返回,队列为空抛出异常
E poll(); // 移除并返回,队列为空返回null
// 查看元素
E element(); // 查看队首元素,队列为空抛出异常
E peek(); // 查看队首元素,队列为空返回null
}
FIFO原则
队列特点:
- 元素从队尾(rear)进入
- 元素从队首(front)取出
- 先进先出,就像排队一样
示意图:
入队: [元素1] [元素2] [元素3] → 队尾
↑ ↓
队首 队尾
出队: [元素1] ← [元素2] ← [元素3] ← 队首
1.1.2 核心方法详解
add() vs offer()
add()方法:
boolean add(E e);
- 添加元素到队尾
- 如果队列已满,抛出
IllegalStateException - 继承自Collection接口
offer()方法:
boolean offer(E e);
- 添加元素到队尾
- 如果队列已满,返回
false - Queue接口特有方法
区别:
- add():失败抛异常
- offer():失败返回false(更安全)
使用建议:
- 有界队列:使用offer(),避免异常
- 无界队列:两者都可以
remove() vs poll()
remove()方法:
E remove();
- 移除并返回队首元素
- 如果队列为空,抛出
NoSuchElementException - 继承自Collection接口
poll()方法:
E poll();
- 移除并返回队首元素
- 如果队列为空,返回
null - Queue接口特有方法
区别:
- remove():队列为空抛异常
- poll():队列为空返回null(更安全)
使用建议:
- 推荐使用poll(),避免异常处理
element() vs peek()
element()方法:
E element();
- 查看队首元素,不移除
- 如果队列为空,抛出
NoSuchElementException - 继承自Collection接口
peek()方法:
E peek();
- 查看队首元素,不移除
- 如果队列为空,返回
null - Queue接口特有方法
区别:
- element():队列为空抛异常
- peek():队列为空返回null(更安全)
使用建议:
- 推荐使用peek(),避免异常处理
1.1.3 方法选择建议
推荐使用的方法
推荐: offer()、poll()、peek()
原因:
- 不会抛出异常
- 返回值明确(true/false或null)
- 代码更简洁
示例:
Queue<String> queue = new LinkedList<>();
// 推荐:使用offer()、poll()、peek()
if (queue.offer("A")) {
// 添加成功
}
String element = queue.poll(); // 可能返回null
if (element != null) {
// 处理元素
}
String head = queue.peek(); // 可能返回null
if (head != null) {
// 查看队首元素
}
1.2 Deque双端队列
1.2.1 Deque接口定义
核心特点
Deque(Double Ended Queue,双端队列)是Queue的子接口,它支持在两端进行插入和删除操作。
public interface Deque<E> extends Queue<E> {
// 队首操作
void addFirst(E e);
boolean offerFirst(E e);
E removeFirst();
E pollFirst();
E getFirst();
E peekFirst();
// 队尾操作
void addLast(E e);
boolean offerLast(E e);
E removeLast();
E pollLast();
E getLast();
E peekLast();
}
双端操作
Deque特点:
- 可以在队首和队尾进行插入和删除
- 既可以作为队列使用,也可以作为栈使用
- 功能更强大,灵活性更高
示意图:
队首 ← → 队尾
↓ ↓
[元素1] [元素2] [元素3]
↑ ↑
可以插入/删除 可以插入/删除
1.2.2 Deque作为栈使用
Stack操作对应关系
Deque可以作为**栈(Stack)**使用,对应关系如下:
| Stack方法 | Deque方法 | 说明 |
|---|---|---|
| push() | addFirst() / offerFirst() | 入栈 |
| pop() | removeFirst() / pollFirst() | 出栈 |
| peek() | peekFirst() | 查看栈顶 |
示例:
// 使用Deque作为栈
Deque<String> stack = new ArrayDeque<>();
// 入栈
stack.push("A"); // 等价于 stack.addFirst("A")
stack.push("B");
stack.push("C");
// 查看栈顶
String top = stack.peek(); // "C"
// 出栈
String element = stack.pop(); // "C",等价于 stack.removeFirst()
为什么推荐使用Deque而不是Stack?
Stack类的问题:
- Stack继承自Vector,性能差
- 所有方法都使用synchronized,开销大
- 设计过时,不推荐使用
Deque的优势:
- 性能更好
- 功能更强大(双端操作)
- 现代设计,推荐使用
1.2.3 Deque作为队列使用
Queue操作对应关系
Deque也可以作为**队列(Queue)**使用,对应关系如下:
| Queue方法 | Deque方法 | 说明 |
|---|---|---|
| offer() | offerLast() | 入队 |
| poll() | pollFirst() | 出队 |
| peek() | peekFirst() | 查看队首 |
示例:
// 使用Deque作为队列
Deque<String> queue = new ArrayDeque<>();
// 入队
queue.offer("A"); // 等价于 queue.offerLast("A")
queue.offer("B");
queue.offer("C");
// 查看队首
String head = queue.peek(); // "A"
// 出队
String element = queue.poll(); // "A",等价于 queue.pollFirst()
1.3 ArrayDeque vs LinkedList
1.3.1 ArrayDeque实现原理
数据结构
ArrayDeque使用循环数组实现:
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable {
// 存储元素的数组
transient Object[] elements;
// 队首索引
transient int head;
// 队尾索引
transient int tail;
}
循环数组:
数组:[null] [A] [B] [C] [null] [null]
↑ ↑
head tail
特点:
- 使用数组存储,内存连续
- 支持循环使用,提高空间利用率
- 性能优于LinkedList
性能特点
时间复杂度:
- 插入:O(1)
- 删除:O(1)
- 查找:O(n)
空间复杂度:
- O(n)
优势:
- 内存连续,CPU缓存友好
- 性能优于LinkedList
- 适合大多数场景
1.3.2 LinkedList实现原理
数据结构
LinkedList使用双向链表实现:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable {
// 双向链表节点
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
// 队首节点
transient Node<E> first;
// 队尾节点
transient Node<E> last;
}
双向链表:
first ← → Node1 ← → Node2 ← → Node3 ← → last
特点:
- 使用链表存储,内存不连续
- 插入删除灵活
- 性能略低于ArrayDeque
性能特点
时间复杂度:
- 插入:O(1)
- 删除:O(1)
- 查找:O(n)
空间复杂度:
- O(n)(每个节点需要额外指针)
优势:
- 不需要预分配容量
- 动态扩容
- 适合元素数量不确定的场景
1.3.3 对比与选择
性能对比
| 特性 | ArrayDeque | LinkedList |
|---|---|---|
| 数据结构 | 循环数组 | 双向链表 |
| 内存占用 | 较小 | 较大(指针开销) |
| CPU缓存 | 友好 | 不友好 |
| 性能 | 更好 | 稍差 |
| 扩容 | 需要扩容 | 不需要 |
选择建议
推荐使用ArrayDeque:
- 大多数场景
- 性能要求高
- 元素数量可预估
使用LinkedList:
- 元素数量不确定
- 需要频繁插入删除中间元素
- 需要List功能
示例:
// 推荐:使用ArrayDeque作为队列
Queue<String> queue = new ArrayDeque<>();
// 推荐:使用ArrayDeque作为栈
Deque<String> stack = new ArrayDeque<>();
// 如果需要List功能,使用LinkedList
List<String> list = new LinkedList<>();
📊 本章总结
核心要点:
- Queue遵循FIFO原则,推荐使用offer()、poll()、peek()
- Deque支持双端操作,可以作为队列和栈使用
- 推荐使用Deque而不是Stack
- ArrayDeque性能优于LinkedList,推荐使用
方法记忆:
- 队列:offer()入队,poll()出队,peek()查看
- 栈:push()入栈,pop()出栈,peek()查看
- 推荐:offer/poll/peek(不抛异常)
第2章:PriorityQueue深度剖析
2.1 PriorityQueue基础原理
2.1.1 数据结构
底层实现
PriorityQueue使用**二叉堆(Binary Heap)**实现:
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
// 存储元素的数组(堆)
transient Object[] queue;
// 元素数量
private int size = 0;
// 比较器
private final Comparator<? super E> comparator;
}
堆的特点:
- 完全二叉树
- 父节点的值总是小于(或大于)子节点的值
- 使用数组存储,节省空间
小顶堆 vs 大顶堆
小顶堆(Min Heap):
- 父节点的值 <= 子节点的值
- 堆顶是最小值
- 默认实现
大顶堆(Max Heap):
- 父节点的值 >= 子节点的值
- 堆顶是最大值
- 需要自定义Comparator
示例:
// 小顶堆(默认)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(3);
minHeap.offer(1);
minHeap.offer(2);
minHeap.poll(); // 1(最小值)
// 大顶堆(自定义Comparator)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
maxHeap.offer(3);
maxHeap.offer(1);
maxHeap.offer(2);
maxHeap.poll(); // 3(最大值)
2.1.2 堆的存储结构
数组存储
堆使用数组存储,父子节点关系通过索引计算:
数组索引: 0 1 2 3 4 5 6
[1] [2] [3] [4] [5] [6] [7]
堆结构:
1
/ \
2 3
/ \ / \
4 5 6 7
索引关系:
- 父节点索引:
(i - 1) / 2 - 左子节点索引:
2 * i + 1 - 右子节点索引:
2 * i + 2
优势:
- 不需要指针,节省空间
- 内存连续,CPU缓存友好
- 索引计算快速
2.2 堆数据结构
2.2.1 堆的性质
完全二叉树
完全二叉树特点:
- 除了最后一层,其他层都是满的
- 最后一层从左到右填充
- 适合用数组存储
堆序性质
小顶堆:
- 父节点的值 <= 左子节点的值
- 父节点的值 <= 右子节点的值
大顶堆:
- 父节点的值 >= 左子节点的值
- 父节点的值 >= 右子节点的值
2.2.2 堆的操作
上浮(Sift Up)
场景: 插入新元素后,需要向上调整
过程:
- 将新元素插入数组末尾
- 与父节点比较
- 如果违反堆序性质,交换
- 重复直到满足堆序性质
示例:
插入元素1:
3
/ \
5 7
/
1 ← 新插入
上浮调整:
1 ← 与父节点3交换
/ \
5 7
/
3
下沉(Sift Down)
场景: 删除堆顶元素后,需要向下调整
过程:
- 将最后一个元素移到堆顶
- 与子节点比较
- 如果违反堆序性质,与较小的子节点交换
- 重复直到满足堆序性质
示例:
删除堆顶1:
1 ← 删除
/ \
3 5
/
7
下沉调整:
3 ← 最后一个元素7移到堆顶
/ \
7 5
继续下沉:
3
/ \
5 7 ← 与较小的子节点5交换
2.3 核心方法实现
2.3.1 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;
}
执行流程:
- 检查null: 不允许null元素
- 扩容检查: 如果数组已满,扩容
- 插入元素: 将元素插入数组末尾
- 上浮调整: 调用siftUp()调整堆
时间复杂度: O(log n)
2.3.2 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;
}
执行流程:
- 检查空队列: 如果为空,返回null
- 获取堆顶: 保存堆顶元素
- 替换堆顶: 将最后一个元素移到堆顶
- 下沉调整: 调用siftDown()调整堆
时间复杂度: O(log n)
2.3.3 peek()方法
实现原理
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
特点:
- 直接返回堆顶元素
- 不移除元素
- 时间复杂度O(1)
2.4 Top K问题应用
2.4.1 什么是Top K问题?
问题定义
Top K问题是指从N个元素中找出最大(或最小)的K个元素。
常见场景:
- 找出最大的K个数
- 找出最小的K个数
- 找出频率最高的K个元素
2.4.2 使用PriorityQueue解决
找最大的K个数
思路: 维护一个大小为K的小顶堆
public List<Integer> topKMax(int[] nums, int k) {
// 小顶堆
PriorityQueue<Integer> heap = new PriorityQueue<>(k);
for (int num : nums) {
if (heap.size() < k) {
heap.offer(num);
} else if (num > heap.peek()) {
// 如果当前数大于堆顶,替换堆顶
heap.poll();
heap.offer(num);
}
}
// 转换为List
List<Integer> result = new ArrayList<>();
while (!heap.isEmpty()) {
result.add(heap.poll());
}
Collections.reverse(result); // 反转,从大到小
return result;
}
原理:
- 小顶堆的堆顶是最小值
- 如果新元素大于堆顶,说明堆顶不是Top K
- 替换堆顶,保持堆大小为K
找最小的K个数
思路: 维护一个大小为K的大顶堆
public List<Integer> topKMin(int[] nums, int k) {
// 大顶堆
PriorityQueue<Integer> heap = new PriorityQueue<>((a, b) -> b - a);
for (int num : nums) {
if (heap.size() < k) {
heap.offer(num);
} else if (num < heap.peek()) {
// 如果当前数小于堆顶,替换堆顶
heap.poll();
heap.offer(num);
}
}
// 转换为List
List<Integer> result = new ArrayList<>();
while (!heap.isEmpty()) {
result.add(heap.poll());
}
return result;
}
原理:
- 大顶堆的堆顶是最大值
- 如果新元素小于堆顶,说明堆顶不是最小的K个
- 替换堆顶,保持堆大小为K
2.4.3 时间复杂度分析
算法复杂度
时间复杂度: O(n log k)
- 遍历n个元素:O(n)
- 每次堆操作:O(log k)
- 总复杂度:O(n log k)
空间复杂度: O(k)
- 堆的大小为K
优势:
- 不需要对所有元素排序
- 适合大数据量场景
- 空间复杂度低
📊 本章总结
核心要点:
- PriorityQueue使用二叉堆实现
- 默认是小顶堆,可以通过Comparator实现大顶堆
- 插入和删除的时间复杂度都是O(log n)
- 适合解决Top K问题
关键记忆:
- 小顶堆:找最大的K个数
- 大顶堆:找最小的K个数
- Top K问题:O(n log k)时间复杂度
第3章:BlockingQueue深度剖析
3.1 BlockingQueue接口基础
3.1.1 接口定义
核心特点
BlockingQueue是Queue的子接口,它提供了阻塞式的插入和删除操作:
public interface BlockingQueue<E> extends Queue<E> {
// 阻塞式插入
void put(E e) throws InterruptedException;
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
// 阻塞式删除
E take() throws InterruptedException;
E poll(long timeout, TimeUnit unit) throws InterruptedException;
// 容量相关
int remainingCapacity();
int drainTo(Collection<? super E> c);
}
阻塞操作
put()方法:
- 插入元素,如果队列已满,阻塞等待
- 直到有空间可用
take()方法:
- 删除并返回元素,如果队列为空,阻塞等待
- 直到有元素可用
优势:
- 自动阻塞,不需要手动等待
- 简化生产者-消费者实现
- 线程安全
3.1.2 主要实现类
实现类对比
| 实现类 | 数据结构 | 有界/无界 | 特点 |
|---|---|---|---|
| ArrayBlockingQueue | 数组 | 有界 | 固定容量,性能好 |
| LinkedBlockingQueue | 链表 | 可选有界 | 默认无界,可指定容量 |
| SynchronousQueue | 无存储 | 无界 | 直接传递,无缓存 |
| DelayQueue | 优先级队列 | 无界 | 延迟元素,按时间排序 |
| PriorityBlockingQueue | 优先级队列 | 无界 | 线程安全的PriorityQueue |
3.2 ArrayBlockingQueue
3.2.1 数据结构
实现原理
ArrayBlockingQueue使用数组实现有界阻塞队列:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 存储元素的数组
final Object[] items;
// 队首索引
int takeIndex;
// 队尾索引
int putIndex;
// 元素数量
int count;
// 锁
final ReentrantLock lock;
// 条件变量
private final Condition notEmpty; // 非空条件
private final Condition notFull; // 非满条件
}
特点:
- 固定容量,创建时指定
- 使用ReentrantLock保证线程安全
- 使用Condition实现阻塞
3.2.2 核心方法实现
put()方法
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();
}
}
执行流程:
- 加锁: 获取ReentrantLock
- 检查容量: 如果队列满,调用notFull.await()阻塞
- 入队: 调用enqueue()插入元素
- 唤醒: 唤醒等待的消费者
- 释放锁: 释放ReentrantLock
take()方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
notEmpty.await(); // 队列空,阻塞等待
}
return dequeue(); // 出队
} finally {
lock.unlock();
}
}
执行流程:
- 加锁: 获取ReentrantLock
- 检查空: 如果队列空,调用notEmpty.await()阻塞
- 出队: 调用dequeue()移除元素
- 唤醒: 唤醒等待的生产者
- 释放锁: 释放ReentrantLock
3.2.3 特点分析
优势
- 性能好: 数组实现,内存连续
- 有界: 固定容量,防止内存溢出
- 线程安全: 使用ReentrantLock保证
劣势
- 锁粒度大: 使用同一个锁,并发度受限
- 固定容量: 不能动态扩容
3.3 LinkedBlockingQueue
3.3.1 数据结构
实现原理
LinkedBlockingQueue使用链表实现阻塞队列:
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 链表节点
static class Node<E> {
E item;
Node<E> next;
}
// 容量(可选)
private final int capacity;
// 当前元素数量
private final AtomicInteger count = new AtomicInteger();
// 队首节点
transient Node<E> head;
// 队尾节点
private transient Node<E> last;
// 出队锁
private final ReentrantLock takeLock = new ReentrantLock();
// 入队锁
private final ReentrantLock putLock = new ReentrantLock();
// 条件变量
private final Condition notEmpty = takeLock.newCondition();
private final Condition notFull = putLock.newCondition();
}
特点:
- 默认无界,可指定容量
- 使用两个锁(takeLock和putLock)
- 提高并发度
3.3.2 锁分离优化
双锁设计
ArrayBlockingQueue:
- 使用一个锁
- 入队和出队互斥
LinkedBlockingQueue:
- 使用两个锁(takeLock和putLock)
- 入队和出队可以并发
优势:
- 提高并发度
- 性能更好
3.3.3 特点分析
优势
- 并发度高: 双锁设计,入队出队可并发
- 可选有界: 可以指定容量,也可以无界
- 动态扩容: 链表实现,不需要预分配
劣势
- 内存占用: 链表节点需要额外指针
- CPU缓存: 内存不连续,缓存不友好
3.4 SynchronousQueue
3.4.1 数据结构
实现原理
SynchronousQueue是一个无存储的阻塞队列:
public class SynchronousQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 内部没有存储元素的数组或链表
// 元素直接从生产者传递给消费者
}
特点:
- 内部没有缓存
- 元素直接从生产者传递给消费者
- 每个put()必须等待一个take()
3.4.2 使用场景
适用场景
直接传递:
- 生产者生产一个元素,必须立即被消费者消费
- 适合任务传递场景
示例:
SynchronousQueue<String> queue = new SynchronousQueue<>();
// 生产者
new Thread(() -> {
try {
queue.put("任务1"); // 阻塞,直到有消费者
queue.put("任务2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 消费者
new Thread(() -> {
try {
String task = queue.take(); // 阻塞,直到有生产者
System.out.println(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
3.4.3 特点分析
优势
- 无缓存: 不占用内存
- 直接传递: 效率高
- 适合任务传递: 线程池使用
劣势
- 必须配对: 每个put()必须对应一个take()
- 阻塞: 如果没有配对的操作,会一直阻塞
3.5 DelayQueue
3.5.1 数据结构
实现原理
DelayQueue是一个延迟队列,元素只有在延迟时间到期后才能被取出:
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
// 底层使用PriorityQueue
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 锁
private final transient ReentrantLock lock = new ReentrantLock();
// 条件变量
private final Condition available = lock.newCondition();
}
特点:
- 元素必须实现Delayed接口
- 底层使用PriorityQueue,按延迟时间排序
- 只有延迟时间到期的元素才能被取出
3.5.2 Delayed接口
接口定义
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
要求:
- 元素必须实现Delayed接口
- getDelay()返回剩余延迟时间
- 必须实现Comparable接口(用于排序)
示例:
class DelayedTask implements Delayed {
private String name;
private long executeTime; // 执行时间(毫秒)
public DelayedTask(String name, long delay) {
this.name = name;
this.executeTime = System.currentTimeMillis() + delay;
}
@Override
public long getDelay(TimeUnit unit) {
long delay = executeTime - System.currentTimeMillis();
return unit.convert(delay, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.executeTime, ((DelayedTask) o).executeTime);
}
}
3.5.3 使用场景
适用场景
延迟任务:
- 定时任务
- 缓存过期
- 订单超时
示例:
DelayQueue<DelayedTask> queue = new DelayQueue<>();
// 添加延迟任务
queue.offer(new DelayedTask("任务1", 5000)); // 5秒后执行
queue.offer(new DelayedTask("任务2", 3000)); // 3秒后执行
// 取出到期的任务
while (true) {
DelayedTask task = queue.take(); // 阻塞,直到有任务到期
System.out.println("执行任务:" + task.getName());
}
3.6 生产者-消费者模式
3.6.1 模式定义
基本概念
生产者-消费者模式:
- 生产者生产数据,放入队列
- 消费者从队列取出数据,进行处理
- 通过队列解耦生产者和消费者
优势:
- 解耦:生产者和消费者互不依赖
- 缓冲:队列作为缓冲区
- 并发:可以多个生产者、多个消费者
3.6.2 使用BlockingQueue实现
简单实现
public class ProducerConsumer {
private BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// 生产者
class Producer implements Runnable {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
String item = "Item " + i;
queue.put(item); // 阻塞式插入
System.out.println("生产:" + item);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 消费者
class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
String item = queue.take(); // 阻塞式取出
System.out.println("消费:" + item);
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public void start() {
new Thread(new Producer()).start();
new Thread(new Consumer()).start();
}
}
多生产者多消费者
public class MultiProducerConsumer {
private BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
public void start() {
// 多个生产者
for (int i = 0; i < 3; i++) {
new Thread(new Producer()).start();
}
// 多个消费者
for (int i = 0; i < 2; i++) {
new Thread(new Consumer()).start();
}
}
}
📊 本章总结
核心要点:
- BlockingQueue提供阻塞式操作,适合生产者-消费者模式
- ArrayBlockingQueue:有界数组,单锁
- LinkedBlockingQueue:可选有界链表,双锁
- SynchronousQueue:无存储,直接传递
- DelayQueue:延迟队列,元素必须实现Delayed接口
选择建议:
- 有界队列:ArrayBlockingQueue
- 高并发:LinkedBlockingQueue
- 任务传递:SynchronousQueue
- 延迟任务:DelayQueue
第4章:Queue集合对比与选择
4.1 Queue实现类对比
4.1.1 非阻塞队列对比
| 特性 | ArrayDeque | LinkedList | PriorityQueue |
|---|---|---|---|
| 数据结构 | 循环数组 | 双向链表 | 二叉堆 |
| 有序性 | FIFO | FIFO | 优先级顺序 |
| 线程安全 | 否 | 否 | 否 |
| 性能 | 最好 | 较好 | O(log n) |
| 适用场景 | 一般队列 | 需要List功能 | 优先级队列 |
4.1.2 阻塞队列对比
| 特性 | ArrayBlockingQueue | LinkedBlockingQueue | SynchronousQueue | DelayQueue |
|---|---|---|---|---|
| 数据结构 | 数组 | 链表 | 无存储 | 优先级队列 |
| 有界/无界 | 有界 | 可选有界 | 无界 | 无界 |
| 锁数量 | 1个 | 2个 | 无 | 1个 |
| 并发度 | 较低 | 较高 | 高 | 较低 |
| 适用场景 | 有界队列 | 高并发 | 任务传递 | 延迟任务 |
4.2 选择指南
4.2.1 非阻塞队列选择
选择流程图
需要Queue?
↓
需要优先级?
├─ 是 → PriorityQueue
└─ 否 → 需要List功能?
├─ 是 → LinkedList
└─ 否 → ArrayDeque(推荐)
4.2.2 阻塞队列选择
选择流程图
需要阻塞队列?
↓
需要延迟?
├─ 是 → DelayQueue
└─ 否 → 需要直接传递?
├─ 是 → SynchronousQueue
└─ 否 → 需要高并发?
├─ 是 → LinkedBlockingQueue
└─ 否 → ArrayBlockingQueue
📊 本章总结
核心要点:
- 非阻塞队列:ArrayDeque性能最好
- 阻塞队列:根据场景选择
- 优先级队列:PriorityQueue
- 延迟队列:DelayQueue
选择建议:
- 一般队列:ArrayDeque
- 阻塞队列:LinkedBlockingQueue(高并发)或ArrayBlockingQueue
- 优先级:PriorityQueue
- 延迟任务:DelayQueue
第5章:Queue集合高频面试题精选
5.1 Queue与Deque面试题
面试题1:Queue接口的add/offer,remove/poll,element/peek这三组方法有什么区别?
答案:
add() vs offer():
| 方法 | 失败行为 | 返回值 |
|---|---|---|
| add() | 抛出IllegalStateException | boolean |
| offer() | 返回false | boolean |
remove() vs poll():
| 方法 | 队列为空行为 | 返回值 |
|---|---|---|
| remove() | 抛出NoSuchElementException | E |
| poll() | 返回null | E |
element() vs peek():
| 方法 | 队列为空行为 | 返回值 |
|---|---|---|
| element() | 抛出NoSuchElementException | E |
| peek() | 返回null | E |
推荐使用:
- offer()、poll()、peek()(不抛异常,更安全)
面试题2:Deque和Queue是什么关系?Deque作为栈(Stack)使用时,对应的方法是什么?
答案:
关系:
- Deque是Queue的子接口
- Deque支持双端操作,功能更强大
作为栈使用:
| Stack方法 | Deque方法 | 说明 |
|---|---|---|
| push() | addFirst() / offerFirst() | 入栈 |
| pop() | removeFirst() / pollFirst() | 出栈 |
| peek() | peekFirst() | 查看栈顶 |
推荐:
- 使用Deque而不是Stack(Stack已过时)
面试题3:作为队列使用,ArrayDeque和LinkedList哪个更好?为什么?
答案:
推荐ArrayDeque:
原因:
- 性能更好: 数组实现,内存连续,CPU缓存友好
- 内存占用小: 不需要指针,节省空间
- 适合大多数场景: 性能要求高
LinkedList优势:
- 需要List功能时使用
- 元素数量不确定时使用
结论:
- 作为队列:推荐ArrayDeque
- 需要List功能:使用LinkedList
5.2 PriorityQueue面试题
面试题4:PriorityQueue的底层数据结构是什么?(二叉堆)
答案:
数据结构:
- PriorityQueue使用**二叉堆(Binary Heap)**实现
- 使用数组存储,节省空间
- 完全二叉树结构
堆的特点:
- 父节点的值 <= 子节点的值(小顶堆)
- 或父节点的值 >= 子节点的值(大顶堆)
- 堆顶是最小值或最大值
面试题5:它是如何保证每次取出的都是最小(或最大)元素的?
答案:
小顶堆保证:
- 堆顶元素是最小值
- poll()方法直接返回堆顶元素
- 删除堆顶后,进行下沉调整,保持堆序性质
大顶堆保证:
- 通过自定义Comparator实现
- 堆顶元素是最大值
- 同样通过堆序性质保证
堆序性质:
- 插入时:上浮调整
- 删除时:下沉调整
- 保证堆顶始终是最小(或最大)值
面试题6:如何用PriorityQueue解决Top K问题?(维护一个大小为K的小顶堆或大顶堆)
答案:
找最大的K个数:
public List<Integer> topKMax(int[] nums, int k) {
// 小顶堆
PriorityQueue<Integer> heap = new PriorityQueue<>(k);
for (int num : nums) {
if (heap.size() < k) {
heap.offer(num);
} else if (num > heap.peek()) {
heap.poll();
heap.offer(num);
}
}
return new ArrayList<>(heap);
}
找最小的K个数:
public List<Integer> topKMin(int[] nums, int k) {
// 大顶堆
PriorityQueue<Integer> heap = new PriorityQueue<>((a, b) -> b - a);
for (int num : nums) {
if (heap.size() < k) {
heap.offer(num);
} else if (num < heap.peek()) {
heap.poll();
heap.offer(num);
}
}
return new ArrayList<>(heap);
}
时间复杂度: O(n log k)
面试题7:PriorityQueue是线程安全的吗?它的线程安全版本是什么?(PriorityBlockingQueue)
答案:
不是线程安全的:
- PriorityQueue不是线程安全的
- 多线程场景需要使用线程安全版本
线程安全版本:
- PriorityBlockingQueue
- 使用ReentrantLock保证线程安全
- 提供阻塞式操作
5.3 BlockingQueue面试题
面试题8:BlockingQueue是什么?它主要解决了什么问题?
答案:
定义:
- BlockingQueue是支持阻塞操作的队列接口
- 提供阻塞式的插入和删除操作
解决的问题:
- 线程安全: 保证多线程环境下的安全操作
- 自动阻塞: 队列满时自动阻塞生产者,队列空时自动阻塞消费者
- 简化实现: 简化生产者-消费者模式的实现
核心方法:
- put():阻塞式插入
- take():阻塞式删除
面试题9:ArrayBlockingQueue和LinkedBlockingQueue的主要区别是什么?(数据结构、锁粒度)
答案:
主要区别:
| 特性 | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|
| 数据结构 | 数组 | 链表 |
| 有界/无界 | 有界 | 可选有界 |
| 锁数量 | 1个 | 2个(takeLock和putLock) |
| 并发度 | 较低 | 较高 |
| 性能 | 较好 | 更好(高并发) |
锁粒度:
- ArrayBlockingQueue:单锁,入队出队互斥
- LinkedBlockingQueue:双锁,入队出队可并发
面试题10:SynchronousQueue有什么特点?它内部有缓存吗?
答案:
特点:
- 无存储:内部没有缓存
- 直接传递:元素直接从生产者传递给消费者
- 必须配对:每个put()必须对应一个take()
内部结构:
- 没有数组或链表存储元素
- 使用Transferer直接传递
- 不占用内存
适用场景:
- 任务传递
- 线程池使用
面试题11:请用代码实现一个简单的生产者-消费者模型,使用BlockingQueue。
答案:
public class ProducerConsumer {
private BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// 生产者
class Producer implements Runnable {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
String item = "Item " + i;
queue.put(item); // 阻塞式插入
System.out.println("生产:" + item);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 消费者
class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
String item = queue.take(); // 阻塞式取出
System.out.println("消费:" + item);
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public void start() {
new Thread(new Producer()).start();
new Thread(new Consumer()).start();
}
}
面试题12:DelayQueue里存放的元素有什么要求?它适用于什么场景?
答案:
元素要求:
- 元素必须实现Delayed接口
- Delayed接口继承Comparable接口
- 必须实现getDelay()和compareTo()方法
接口定义:
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
适用场景:
- 定时任务
- 缓存过期
- 订单超时
- 延迟执行
示例:
class DelayedTask implements Delayed {
private String name;
private long executeTime;
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.executeTime, ((DelayedTask) o).executeTime);
}
}
5.4 Queue集合综合面试题
面试题13:Queue和Stack的区别是什么?
答案:
Queue(队列):
- FIFO(先进先出)
- 队尾入队,队首出队
- 接口:Queue、Deque
Stack(栈):
- LIFO(后进先出)
- 栈顶入栈,栈顶出栈
- 类:Stack(已过时),推荐使用Deque
面试题14:如何选择合适的Queue实现?
答案:
选择指南:
- 一般队列: ArrayDeque(性能最好)
- 需要List功能: LinkedList
- 需要优先级: PriorityQueue
- 需要阻塞: BlockingQueue
- 有界:ArrayBlockingQueue
- 高并发:LinkedBlockingQueue
- 任务传递:SynchronousQueue
- 延迟任务:DelayQueue
面试题15:PriorityQueue的时间复杂度是多少?
答案:
时间复杂度:
- 插入(offer):O(log n)
- 删除(poll):O(log n)
- 查看(peek):O(1)
原因:
- 二叉堆的高度是log n
- 插入和删除需要调整堆,最多调整log n次
面试题16:BlockingQueue的put()和take()方法会一直阻塞吗?
答案:
会阻塞,但可以中断:
- put():队列满时阻塞,直到有空间或线程被中断
- take():队列空时阻塞,直到有元素或线程被中断
中断处理:
- 方法声明抛出InterruptedException
- 可以通过interrupt()中断阻塞
面试题17:如何实现一个线程安全的队列?
答案:
方案1:使用BlockingQueue
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
方案2:使用ConcurrentLinkedQueue
Queue<String> queue = new ConcurrentLinkedQueue<>();
方案3:使用Collections.synchronizedQueue()
Queue<String> queue = Collections.synchronizedQueue(new LinkedList<>());
面试题18:PriorityQueue的默认排序是什么?如何实现大顶堆?
答案:
默认排序:
- 小顶堆(最小堆)
- 堆顶是最小值
实现大顶堆:
// 使用自定义Comparator
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
// 或
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
面试题19:ArrayBlockingQueue和LinkedBlockingQueue的容量有什么区别?
答案:
ArrayBlockingQueue:
- 必须指定容量
- 固定容量,不能改变
LinkedBlockingQueue:
- 可以指定容量,也可以不指定
- 不指定时,默认Integer.MAX_VALUE(无界)
- 指定时,有界队列
面试题20:如何优化Queue的性能?
答案:
优化策略:
-
选择合适的实现:
- 一般队列:ArrayDeque
- 阻塞队列:LinkedBlockingQueue(高并发)
-
预分配容量:
Queue<String> queue = new ArrayDeque<>(expectedSize); -
使用合适的阻塞队列:
- 高并发:LinkedBlockingQueue(双锁)
- 有界:ArrayBlockingQueue
-
避免不必要的操作:
- 使用offer/poll/peek而不是add/remove/element