Java 队列全面详解:从入门到面试实战

1. 什么是队列?

队列(Queue)是一种操作受限的线性数据结构,遵循**先进先出(FIFO, First In First Out)**原则。它只允许在一端(队尾)进行插入操作,在另一端(队头)进行删除操作。队列就像日常生活中的排队,先进入队列的元素会先被处理。

1.1 队列的常见应用场景

  • 任务调度:线程池中的任务队列,CPU 的任务调度。
  • 消息队列:异步处理、系统解耦(Kafka、RabbitMQ 底层也用到了队列思想)。
  • 广度优先搜索(BFS):图的遍历、树的层序遍历。
  • 缓存系统:如 LRU Cache 的实现(常与链表或队列结合)。
  • 打印机任务队列:多个打印任务按顺序打印。

2. Java 中的队列接口体系

Java 的队列相关接口主要定义在 java.util 包下,它们之间存在清晰的继承关系:

接口 描述
Collection 所有集合类的根接口。
Iterable 支持迭代。
Queue<E> 队列接口,定义了基本的添加、删除、检查操作。
Deque<E> 双端队列,两端都可插入和删除。
BlockingQueue<E> 阻塞队列接口,提供了阻塞的插入和移除操作。
BlockingDeque<E> 阻塞双端队列。
TransferQueue<E> 特殊的阻塞队列,生产者会等待消费者接收元素。

主要关注 QueueDequeBlockingQueue 这三个核心接口。

3. 常用队列实现类

3.1 LinkedList(链表实现的队列)

LinkedList 同时实现了 ListDeque 接口,因此它既可以当作列表,也可以当作普通队列和双端队列使用。底层使用双向链表,插入删除效率高,无容量限制。

java 复制代码
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
queue.offer(2);
System.out.println(queue.poll());  // 1,出队
System.out.println(queue.peek()); // 2,查看队头

3.2 PriorityQueue(优先队列)

PriorityQueue 基于优先级堆(默认小顶堆)实现,元素按照自然顺序或自定义比较器排序。出队时总是返回优先级最高的元素(最小值或最大值)。

java 复制代码
// 默认小顶堆
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(3);
pq.offer(1);
pq.offer(2);
System.out.println(pq.poll()); // 1,按优先级出队

注意:PriorityQueue 不是线程安全的,也不允许插入 null 元素。

3.3 ArrayDeque(数组双端队列)

ArrayDeque 是基于循环数组实现的双端队列,可作为栈或队列使用。它比 LinkedList 性能更好(数组缓存友好,无额外节点开销),因此官方推荐用 ArrayDeque 替代 Stack 和大多数 LinkedList 的队列场景。

java 复制代码
Deque<Integer> deque = new ArrayDeque<>();
deque.offerFirst(1); // 队头入队
deque.offerLast(2);  // 队尾入队
System.out.println(deque.pollFirst()); // 1

3.4 并发队列:ConcurrentLinkedQueue

ConcurrentLinkedQueue 是基于链表实现的无界线程安全队列,使用 CAS(Compare-And-Swap)无锁算法保证并发安全,适用于高并发场景。

java 复制代码
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
queue.offer(1);
queue.poll();

3.5 阻塞队列家族

阻塞队列是 JUC(java.util.concurrent)包中的重要组件,常用于生产者-消费者模型。

  • ArrayBlockingQueue:有界阻塞队列,基于数组实现,使用一把锁控制入队和出队。
  • LinkedBlockingQueue:可选有界(默认无界,最大 Integer.MAX_VALUE),基于链表实现,使用两把锁分离入队和出队,吞吐量更高。
  • PriorityBlockingQueue:无界的优先级阻塞队列。
  • DelayQueue:延迟队列,元素只有在其指定的延迟时间到达后才能被取出。
  • SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待一个移除操作,常用于线程池的任务交接。
java 复制代码
// 生产者消费者简单示例
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
new Thread(() -> {
    for (int i = 0; i < 100; i++) {
        try {
            queue.put(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

4. 队列常用方法对比

队列操作主要分为三类:添加元素、移除元素、查看队头元素。每种操作又分"出错时抛异常"和"返回特殊值"两种风格。

操作 抛异常方法 返回特殊值方法 说明
添加 add(e) offer(e) offer 在容量满时返回 false,add 抛异常
移除 remove() poll() poll 在队列空时返回 null,remove 抛异常
检查 element() peek() peek 在队列空时返回 null,element 抛异常

双端队列额外方法:

操作 抛异常方法 返回特殊值方法
队头添加 addFirst(e) offerFirst(e)
队尾添加 addLast(e) offerLast(e)
队头移除 removeFirst() pollFirst()
队尾移除 removeLast() pollLast()
队头检查 getFirst() peekFirst()
队尾检查 getLast() peekLast()

5. 优先队列原理与实现

优先队列通常使用堆(Heap) 实现。Java 中的 PriorityQueue 默认采用小顶堆,即在完全二叉树中,每个父节点的值都小于等于子节点的值,因此堆顶总是最小值。

入队(offer)时,新元素被放到数组末尾,然后执行上浮(siftUp) 操作,不断与父节点比较交换,保证堆的性质。出队(poll)时,移除堆顶元素,将数组最后一个元素移到堆顶,再执行**下沉(siftDown)**操作,与较小的子节点交换,直到满足堆条件。

时间复杂度:入队 O(log n),出队 O(log n),查看堆顶 O(1)。

java 复制代码
// 自定义比较器实现大顶堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.offer(3);
maxHeap.offer(5);
System.out.println(maxHeap.poll()); // 5,最大值先出队

6. 阻塞队列原理与生产者消费者模式

BlockingQueue 通过锁(ReentrantLock)和条件(Condition)实现线程间的等待/通知机制。put(e) 方法在队列满时,会在 notFull 条件上等待;take() 方法在队列空时,在 notEmpty 条件上等待。当有元素被取出或加入时,会唤醒相应的等待线程。

在经典的生产者消费者模式中,生产者线程调用 put 生产数据,消费者线程调用 take 消费数据,阻塞队列负责缓冲和同步,极大地简化了并发编程。

java 复制代码
class Producer implements Runnable {
    private BlockingQueue<Integer> queue;
    public Producer(BlockingQueue<Integer> q) { this.queue = q; }
    public void run() {
        for (int i = 0; i < 100; i++) {
            try { queue.put(i); } catch (InterruptedException e) { }
        }
    }
}
class Consumer implements Runnable {
    private BlockingQueue<Integer> queue;
    public Consumer(BlockingQueue<Integer> q) { this.queue = q; }
    public void run() {
        while (true) {
            try {
                Integer val = queue.take();
                System.out.println("consume " + val);
            } catch (InterruptedException e) { }
        }
    }
}

7. 双端队列 Deque 详解

Deque 接口扩展了 Queue,允许在头尾进行快速插入和移除。主要实现类有:

  • ArrayDeque:基于循环数组,性能优秀,建议优先使用。
  • LinkedList:基于链表,适合频繁增删的场景。

Deque 可以模拟栈:push() 对应 addFirst()pop() 对应 removeFirst()。因此官方建议使用 Deque<Integer> stack = new ArrayDeque<>(); 来替代古老的 Stack 类。

java 复制代码
Deque<Integer> deque = new ArrayDeque<>();
deque.push(1); // 入栈
deque.push(2);
System.out.println(deque.pop()); // 2,栈顶元素

8. 队列的线程安全与选择策略

在多线程环境下需要根据场景选择合适的队列:

  • 简单的并发场景ConcurrentLinkedQueue,无界、无锁,性能好。
  • 需要阻塞等待的生产者消费者 :使用 ArrayBlockingQueueLinkedBlockingQueue。如果有明确的容量限制,优先用 ArrayBlockingQueue;若需要更高吞吐量且可接受较大内存占用,用 LinkedBlockingQueue
  • 延迟处理任务DelayQueue
  • 任务交接无缓冲SynchronousQueue
  • 需要两端操作的并发LinkedBlockingDeque

注意:Collections.synchronizedList(new LinkedList()); 包装的队列虽然线程安全,但性能远不如并发专用类,且不支持阻塞操作。

9. 经典面试题解析

9.1 用两个栈实现队列

思路:栈是先进后出,两个栈倒一下就能实现先进先出。一个栈用于入队,另一个用于出队。当出队栈为空时,将入队栈的所有元素倒入出队栈。

java 复制代码
class MyQueue {
    private Stack<Integer> inStack, outStack;
    public MyQueue() { inStack = new Stack<>(); outStack = new Stack<>(); }
    public void push(int x) { inStack.push(x); }
    public int pop() {
        if (outStack.isEmpty())
            while (!inStack.isEmpty()) outStack.push(inStack.pop());
        return outStack.pop();
    }
    public int peek() {
        if (outStack.isEmpty())
            while (!inStack.isEmpty()) outStack.push(inStack.pop());
        return outStack.peek();
    }
    public boolean empty() { return inStack.isEmpty() &&& outStack.isEmpty(); }
}

9.2 用队列实现栈

思路:使用两个队列,或一个队列。入栈时直接入队;出栈时将队列前 n-1 个元素出队再入队,此时队头即为栈顶。

java 复制代码
class MyStack {
    Queue<Integer> queue = new LinkedList<>();
    public void push(int x) {
        queue.offer(x);
        for (int i = 0; i < queue.size() - 1; i++)
            queue.offer(queue.poll());
    }
    public int pop() { return queue.poll(); }
    public int top()  { return queue.peek(); }
    public boolean empty() { return queue.isEmpty(); }
}

9.3 优先队列解决 TopK 问题

求数组中第 K 大的元素或前 K 个最大的元素:用一个大小为 K 的小顶堆,遍历数组,如果当前元素大于堆顶,则移除堆顶并加入当前元素。最终堆顶就是第 K 大的元素。

java 复制代码
public int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> pq = new PriorityQueue<>();
    for (int num : nums) {
        pq.offer(num);
        if (pq.size() > k) pq.poll();
    }
    return pq.peek();
}

9.4 滑动窗口最大值

使用双端队列(Deque)维护一个递减的窗口。队列存储数组的下标,队头始终是窗口内的最大值。当窗口滑动时,移除过期的下标,保持队头合法。

java 复制代码
public int[] maxSlidingWindow(int[] nums, int k) {
    if (nums == null || nums.length == 0) return new int[0];
    int[] res = new int[nums.length - k + 1];
    Deque<Integer> deque = new ArrayDeque<>();
    for (int i = 0; i < nums.length; i++) {
        if (!deque.isEmpty() && deque.peekFirst() <= i - k) deque.pollFirst();
        while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) deque.pollLast();
        deque.offerLast(i);
        if (i >= k - 1) res[i - k + 1] = nums[deque.peekFirst()];
    }
    return res;
}

9.5 设计一个延迟任务调度器

利用 DelayQueue 实现。每个任务实现 Delayed 接口,定义延迟时间。消费者线程从队列中 take() 任务,只有到达指定时间才会被取出执行。

java 复制代码
class Task implements Delayed {
    private long time;
    private String name;
    public Task(long delay, String name) {
        this.time = System.currentTimeMillis() + delay;
        this.name = name;
    }
    @Override public long getDelay(TimeUnit unit) {
        return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
    @Override public int compareTo(Delayed o) {
        return Long.compare(this.time, ((Task)o).time);
    }
    public void execute() { System.out.println("Run " + name); }
}

public class Scheduler {
    public static void main(String[] args) throws InterruptedException {
        DelayQueue<Task> queue = new DelayQueue<>();
        queue.put(new Task(3000, "task1"));
        queue.put(new Task(1000, "task2"));
        while (!queue.isEmpty()) {
            Task t = queue.take(); // 阻塞直到任务到期
            t.execute();
        }
    }
}

9.6 常见面试简答题

  • ArrayBlockingQueue 和 LinkedBlockingQueue 的区别? 前者基于数组,容量固定,一把锁控制生产和消费;后者基于链表,默认无界,使用两把锁分离读写,吞吐量更高。
  • ConcurrentLinkedQueue 与 BlockingQueue 的区别? 前者无锁非阻塞,使用 CAS;后者阻塞,使用锁和条件。
  • PriorityQueue 的底层数据结构是什么? 二叉堆(数组实现的完全二叉树)。
  • 队列和栈的区别? 队列 FIFO,栈 LIFO;队列用于缓冲、调度,栈用于递归、撤销操作等。
  • 如何实现一个线程安全的队列? 可直接使用 JUC 下的队列类,或者用 Collections.synchronizedList 包装,但性能不如前者。

10. 总结

本文从 Java 队列的接口体系出发,详细介绍了普通队列、优先队列、双端队列以及阻塞队列的实现原理和使用场景。重点掌握了:

  • 队列的 FIFO 特性及其典型应用。
  • Queue、Deque、BlockingQueue 三大接口及其核心方法。
  • PriorityQueue 的堆实现和 TopK 面试题。
  • 阻塞队列在生产者消费者模式中的应用。
  • ConcurrentLinkedQueue 等并发队列的选择策略。
  • 常见的队列相关算法面试题。

理解这些内容,能够帮助你在实际开发中合理选择队列实现,并轻松应对大部分 Java 队列相关的面试。建议结合源码进一步深入,比如分析 ArrayBlockingQueueputtake 方法的锁实现,将理论与实践结合。

相关推荐
AI直播技术杂谈1 小时前
AI直播推流链路中延迟优化的通用技术方案
开发语言·人工智能·php
vx-程序开发1 小时前
springboot农产品运输服务平台---附源码75498
java·javascript·spring boot·python·eclipse·django·php
Dxy12393102162 小时前
python 中的 APScheduler 使用详解
开发语言·python
zzh___zzh2 小时前
SQL 窗口函数 ROW_NUMBER、RANK、DENSE_RANK 常用方法笔记
java
计科土狗3 小时前
GESP六级专题之类与对象
java·前端·数据库
福如意如我心意3 小时前
java虚拟线程和Go协程的区别
开发语言
Slow菜鸟3 小时前
第3篇:实操落地篇 · 3套企业标准工作流(IDEA可视化版)
java·ide·intellij-idea
weixin_440784113 小时前
Java基础常见题
java·开发语言·python·java基础
掉鱼的猫3 小时前
Hot-Plug 实战:运行期管理 Solon 插件
java·hotfix