四大集合之Queue-用不一样的姿势学习

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接口,那这个队列就可以使用addFirstaddLast等双端队列特有的方法。

有朋友就会问:那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):将元素插入队列,返回 truefalse

  • 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 有什么不同?

答: PriorityBlockingQueuePriorityQueue 的线程安全版本,它是一个阻塞队列,支持元素按优先级排序。区别在于:

  • 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 时可能会发生死锁。为了避免死锁,可以:

  1. 使用有界队列:设置队列的最大容量,防止队列无限增长导致内存耗尽。

  2. 超时机制:使用带超时的插入和移除操作,如 offer(E e, long timeout, TimeUnit unit)poll(long timeout, TimeUnit unit),避免永远阻塞。

  3. 合理设计线程池:确保生产者和消费者的线程数相匹配,避免生产者或消费者线程饥饿。


15. 在什么场景下会使用 LinkedBlockingQueue 而不是 ArrayBlockingQueue?

答: LinkedBlockingQueueArrayBlockingQueue 都是阻塞队列,但它们有以下不同:

  • LinkedBlockingQueue 基于链表实现,可以是有界或无界,适合对队列大小不确定的场景。

  • ArrayBlockingQueue 基于数组实现,是有界队列,适合需要限制内存使用的场景。

如果队列大小不固定或希望动态扩展,使用 LinkedBlockingQueue 更为合适;如果需要对队列大小进行限制,避免内存溢出,ArrayBlockingQueue 更合适

 结束啦,希望大家能有所成!!!

 

 你好,我是胡广。 致力于为帮助兄弟们的学习方式、面试困难、入职经验少走弯路而写博客 🌹🌹🌹 坚持每天两篇高质量文章输出,加油!!!🤩

 如果本篇文章帮到了你 不妨点个赞吧~ 我会很高兴的 😄 (^ ~ ^) 。想看更多 那就点个关注     吧 我会尽力带来有趣的内容 。

 😎感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以      给我留言咨询,希望帮助更多的人

更多专栏:
📊 Java设计模式宝典:从入门到精通(持续更新)

📝 Java基础知识:GoGoGo(持续更新)

Java面试宝典:从入门到精通(持续更新)

🌟 程序员的那些事~(乐一乐)

🤩 Redis知识、及面试(持续更新)

🚀 Kafka知识文章专栏(持续更新)

🎨 Nginx知识讲解专栏(持续更新)

📡 未完待续。。。

🎯 未完待续。。。

🔍 未完待续。。。

感谢订阅专栏 三连文章

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员-杨胡广

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值