Java集合篇———Queue

264 阅读23分钟

第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 对比与选择

性能对比
特性ArrayDequeLinkedList
数据结构循环数组双向链表
内存占用较小较大(指针开销)
CPU缓存友好不友好
性能更好稍差
扩容需要扩容不需要
选择建议

推荐使用ArrayDeque:

  • 大多数场景
  • 性能要求高
  • 元素数量可预估

使用LinkedList:

  • 元素数量不确定
  • 需要频繁插入删除中间元素
  • 需要List功能

示例:

// 推荐:使用ArrayDeque作为队列
Queue<String> queue = new ArrayDeque<>();

// 推荐:使用ArrayDeque作为栈
Deque<String> stack = new ArrayDeque<>();

// 如果需要List功能,使用LinkedList
List<String> list = new LinkedList<>();

📊 本章总结

核心要点:

  1. Queue遵循FIFO原则,推荐使用offer()、poll()、peek()
  2. Deque支持双端操作,可以作为队列和栈使用
  3. 推荐使用Deque而不是Stack
  4. 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. 将新元素插入数组末尾
  2. 与父节点比较
  3. 如果违反堆序性质,交换
  4. 重复直到满足堆序性质

示例:

插入元素1:
     3
   /   \
  5     7
 /
1  ← 新插入

上浮调整:
     1  ← 与父节点3交换
   /   \
  5     7
 /
3
下沉(Sift Down)

场景: 删除堆顶元素后,需要向下调整

过程:

  1. 将最后一个元素移到堆顶
  2. 与子节点比较
  3. 如果违反堆序性质,与较小的子节点交换
  4. 重复直到满足堆序性质

示例:

删除堆顶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;
}

执行流程:

  1. 检查null: 不允许null元素
  2. 扩容检查: 如果数组已满,扩容
  3. 插入元素: 将元素插入数组末尾
  4. 上浮调整: 调用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;
}

执行流程:

  1. 检查空队列: 如果为空,返回null
  2. 获取堆顶: 保存堆顶元素
  3. 替换堆顶: 将最后一个元素移到堆顶
  4. 下沉调整: 调用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

优势:

  • 不需要对所有元素排序
  • 适合大数据量场景
  • 空间复杂度低

📊 本章总结

核心要点:

  1. PriorityQueue使用二叉堆实现
  2. 默认是小顶堆,可以通过Comparator实现大顶堆
  3. 插入和删除的时间复杂度都是O(log n)
  4. 适合解决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();
    }
}

执行流程:

  1. 加锁: 获取ReentrantLock
  2. 检查容量: 如果队列满,调用notFull.await()阻塞
  3. 入队: 调用enqueue()插入元素
  4. 唤醒: 唤醒等待的消费者
  5. 释放锁: 释放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();
    }
}

执行流程:

  1. 加锁: 获取ReentrantLock
  2. 检查空: 如果队列空,调用notEmpty.await()阻塞
  3. 出队: 调用dequeue()移除元素
  4. 唤醒: 唤醒等待的生产者
  5. 释放锁: 释放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();
        }
    }
}

📊 本章总结

核心要点:

  1. BlockingQueue提供阻塞式操作,适合生产者-消费者模式
  2. ArrayBlockingQueue:有界数组,单锁
  3. LinkedBlockingQueue:可选有界链表,双锁
  4. SynchronousQueue:无存储,直接传递
  5. DelayQueue:延迟队列,元素必须实现Delayed接口

选择建议:

  • 有界队列:ArrayBlockingQueue
  • 高并发:LinkedBlockingQueue
  • 任务传递:SynchronousQueue
  • 延迟任务:DelayQueue

第4章:Queue集合对比与选择

4.1 Queue实现类对比

4.1.1 非阻塞队列对比

特性ArrayDequeLinkedListPriorityQueue
数据结构循环数组双向链表二叉堆
有序性FIFOFIFO优先级顺序
线程安全
性能最好较好O(log n)
适用场景一般队列需要List功能优先级队列

4.1.2 阻塞队列对比

特性ArrayBlockingQueueLinkedBlockingQueueSynchronousQueueDelayQueue
数据结构数组链表无存储优先级队列
有界/无界有界可选有界无界无界
锁数量1个2个1个
并发度较低较高较低
适用场景有界队列高并发任务传递延迟任务

4.2 选择指南

4.2.1 非阻塞队列选择

选择流程图
需要Queue?
    ↓
需要优先级?
    ├─ 是 → PriorityQueue
    └─ 否 → 需要List功能?
            ├─ 是 → LinkedList
            └─ 否 → ArrayDeque(推荐)

4.2.2 阻塞队列选择

选择流程图
需要阻塞队列?
    ↓
需要延迟?
    ├─ 是 → DelayQueue
    └─ 否 → 需要直接传递?
            ├─ 是 → SynchronousQueue
            └─ 否 → 需要高并发?
                    ├─ 是 → LinkedBlockingQueue
                    └─ 否 → ArrayBlockingQueue

📊 本章总结

核心要点:

  1. 非阻塞队列:ArrayDeque性能最好
  2. 阻塞队列:根据场景选择
  3. 优先级队列:PriorityQueue
  4. 延迟队列:DelayQueue

选择建议:

  • 一般队列:ArrayDeque
  • 阻塞队列:LinkedBlockingQueue(高并发)或ArrayBlockingQueue
  • 优先级:PriorityQueue
  • 延迟任务:DelayQueue

第5章:Queue集合高频面试题精选

5.1 Queue与Deque面试题

面试题1:Queue接口的add/offer,remove/poll,element/peek这三组方法有什么区别?

答案:

add() vs offer():

方法失败行为返回值
add()抛出IllegalStateExceptionboolean
offer()返回falseboolean

remove() vs poll():

方法队列为空行为返回值
remove()抛出NoSuchElementExceptionE
poll()返回nullE

element() vs peek():

方法队列为空行为返回值
element()抛出NoSuchElementExceptionE
peek()返回nullE

推荐使用:

  • 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:

原因:

  1. 性能更好: 数组实现,内存连续,CPU缓存友好
  2. 内存占用小: 不需要指针,节省空间
  3. 适合大多数场景: 性能要求高

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是支持阻塞操作的队列接口
  • 提供阻塞式的插入和删除操作

解决的问题:

  1. 线程安全: 保证多线程环境下的安全操作
  2. 自动阻塞: 队列满时自动阻塞生产者,队列空时自动阻塞消费者
  3. 简化实现: 简化生产者-消费者模式的实现

核心方法:

  • put():阻塞式插入
  • take():阻塞式删除
面试题9:ArrayBlockingQueue和LinkedBlockingQueue的主要区别是什么?(数据结构、锁粒度)

答案:

主要区别:

特性ArrayBlockingQueueLinkedBlockingQueue
数据结构数组链表
有界/无界有界可选有界
锁数量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实现?

答案:

选择指南:

  1. 一般队列: ArrayDeque(性能最好)
  2. 需要List功能: LinkedList
  3. 需要优先级: PriorityQueue
  4. 需要阻塞: 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的性能?

答案:

优化策略:

  1. 选择合适的实现:

    • 一般队列:ArrayDeque
    • 阻塞队列:LinkedBlockingQueue(高并发)
  2. 预分配容量:

    Queue<String> queue = new ArrayDeque<>(expectedSize);
    
  3. 使用合适的阻塞队列:

    • 高并发:LinkedBlockingQueue(双锁)
    • 有界:ArrayBlockingQueue
  4. 避免不必要的操作:

    • 使用offer/poll/peek而不是add/remove/element