队列的使用详解

队列的定义

队列是一种特殊的线性表,遵循的原则就是“先入先出”。在我们日常使用中,经常会用来并发操作数据。在并发编程中,有时候需要使用线程安全的队列。如果要实现一个线程安全的队列通常有两种方式:一种是使用阻塞队列,另一种是使用线程同步锁。

队列的继承关系图

队列的概述

队列是一种重要的抽象数据结构,可类比于生活中的排队场景Java语言提供了队列的支持,内置了多种类型的队列供我们使用,队列数据存储显著特点就是先进先出,类似列车进涵洞。在并发场景,电商秒杀、队列的用处很大。

队列(Queue):队列简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。向队列中插入元素称为入队或进队;删除元素称为出队或离队。

这和我们日常生活中的排队是一致的,最早排队的也是最早离队的。其操作的特性是先进先出 (First In First Out, FIFO),故又称为先进先出的线性表。

队头(Front):允许删除的一端,又称为队首。

队尾(Rear):允许插入的一端。

空队列:不含任何元素的空表。

队列的基本操作

  • InitQueue(&Q):初始化队列,构造一个空队列Q。

  • QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。

  • EnQueue(&Q, x):入队,若队列Q未满,将x加入,使之成为新的队尾。

  • DeQueue(&Q, &x):出队,若队列Q非空,删除队头元素,并用x返回。

  • GetHead(Q, &x):读队头元素,若队列Q非空,则将队头元素赋值给X。

  • 我们常用的 LinkedList 集合,它实现了Queue 接口,因此,我们可以理解为 LinkedList 就是一个队列。

需要注意的是,队列是操作受限的线性表,所以,不是任何对线性表的操作都可以作为队列的操作。比如,不可以随便读取队列中间的某个数据。

队列的使用场景

消息队列是用来解决这样的问题的:将突发的大量请求转换位服务器能够处理的队列请求。eg:在一个秒杀活动中,服务器1秒可以处理100条请求。而在秒杀活动开启1秒进来1000个请求并且持续10秒。这个时候就需要将这10000个请求放入消息队列里面,后端按照原来的能力处理,用100秒将队列中的请求处理完毕。这样就不会宕机。

java队列特性

队列主要分为阻塞和非阻塞,有界和无界、单向链表和双向链表之分;

阻塞和非阻塞

阻塞队列

入列(添加元素)时,如果元素数量超过队列总数,会进行等待(阻塞),待队列的中的元素出列后,元素数量未超过队列总数时,就会解除阻塞状态,进而可以继续入列;

出列(删除元素)时,如果队列为空的情况下,也会进行等待(阻塞),待队列有值的时候即会解除阻塞状态,进而继续出列;

阻塞队列的好处是可以防止队列容器溢出;只要满了就会进行阻塞等待;也就不存在溢出的情况;

只要是阻塞队列,都是线程安全的;

非阻塞队列

不管出列还是入列,都不会进行阻塞,

入列时,如果元素数量超过队列总数,则会抛出异常,

出列时,如果队列为空,则取出空值;

一般情况下,非阻塞式队列使用的比较少,一般都用阻塞式的对象比较多;阻塞和非阻塞队列在使用上的最大区别就是阻塞队列提供了以下2个方法:

出队阻塞方法 : take()

入队阻塞方法 : put()

有界和无界

有界:有界限,大小长度受限制

无界:无限大小,其实说是无限大小,其实是有界限的,只不过超过界限时就会进行扩容,就行ArrayList 一样,在内部动态扩容单向链表和双向链表

单向链表 : 每个元素中除了元素本身之外,还存储一个指针,这个指针指向下一个元素;

**双向链表 :**除了元素本身之外,还有两个指针,一个指针指向前一个元素的地址,另一个指针指向后一个元素的地址;

队列常用方法

方法名

说明

add

增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常

remove

移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常

element

返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常

offer

添加一个元素并返回true 如果队列已满,则返回false

poll

移除并返问队列头部的元素 如果队列为空,则返回null

peek

返回队列头部的元素 如果队列为空,则返回null

put

添加一个元素 如果队列满,则阻塞

take

移除并返回队列头部的元素 如果队列为空,则阻塞

drainTo(list)

一次性取出队列所有元素

java中队列的实现类

非阻塞队列

  1. ConcurrentLinkedQueue

单向链表结构的无界并发队列, 非阻塞队列,由CAS实现线程安全,内部基于节点实现

  1. ConcurrentLinkedDeque

双向链表结构的无界并发队列, 非阻塞队列,由CAS实现线程安全

  1. PriorityQueue

内部基于数组实现,线程不安全的队列

阻塞队列

  1. DelayQueue

一个支持延时获取元素的无界阻塞队列

  1. LinkedTransferQueue

一个由链表结构组成的无界阻塞队列。

  1. ArrayBlockingQueue

有界队列,阻塞式,初始化时必须指定队列大小,且不可改变;,底层由数组实现;

  1. SynchronousQueue

最多只能存储一个元素,每一个put操作必须等待一个take操作,否则不能继续添加元素

  1. PriorityBlockingQueue

一个带优先级的队列,而不是先进先出队列。元素按优先级顺序被移除,而且它也是无界的,也就是没有容量上限,虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError 错误

阻塞队列案例

阻塞队列(Blocking Queue)提供了可阻塞的put和take方法,它们与可定时的offer和pull是等价的。如果队列满了put方法会被阻塞等到有空间可用再将元素插入;如果队列是空的,那么take方法也会阻塞,直到有元素可用。当队列永远不会被充满时,put方法和take方法就永远不会阻塞。

我们可以从队列的名称中知道此队列是否为阻塞队列,阻塞队列中包含BlockingQueue关键字,比如以下:

  • ArrayBlockingQueue

  • LinkedBlockingQueue

  • PriorityBlockingQueue

阻塞功能队列演示:

public class BlockingTest {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个长度为 5 的阻塞队列
        ArrayBlockingQueue q1 = new ArrayBlockingQueue(5);

        // 新创建一个线程执行入列
        new Thread(() -> {
            // 循环 10 次
            for (int i = 0; i < 10; i++) {
                try {
                    q1.put(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(new Date() + " | ArrayBlockingQueue Size:" + q1.size());
            }
            System.out.println(new Date() + " | For End.");
        }).start();

        // 新创建一个线程执行出列
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    // 休眠 1S
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (!q1.isEmpty()) {
                    try {
                        q1.take(); // 出列
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

执行结果:

从上述结果可以看出,当==ArrayBlockingQueue== 队列满了之后就会进入阻塞,当过了 1 秒有元素从队列中移除之后,才会将新的元素入列。

非阻塞列

非阻塞队列也就是普通队列,它的名字不会包含BlockingQueue关键字,并且它不会包含put和take方法,当队列满之后如果还有新元素入列会直接返回错误,并不会阻塞的等待着添加元素。

非阻塞队列的典型代表是 ConcurrentLinkedQueue和 PriorityQueue。

有界队列和无界队列

有界队列:是指有固定大小的队列,比如设定了固定大小的ArrayBlockingQueue,又或者大小为0的SychronousQueue。

无界队列:指的是没有设置固定大小的队列,但其实如果没有设置固定大小也是有默认值的,只不过默认值是Integer.MAX_VALUE,当然实际的使用中不会有这么大的容量(超过Integer.MAX_VALUE),所以从使用者的角度来看相当于”无界“的。

按功能分类

我们以功能来划分一下队列,它可以被分为:普通队列、优先队列、双端队列、延迟队列、其他队列等,接下来我们分别来看。

1.普通队列

普通队列(Queue)是指实现了先进先出的基本队列,例如 ArrayBlockingQueue 和 LinkedBlockingQueue,其中 ArrayBlockingQueue是用数组实现的普通队列,如下图所示:

而 LinkedBlockingQueue 是使用链表实现的普通队列,如下图所示:

LinkedBlockingQueue演示普通队列的使用:

import java.util.concurrent.LinkedBlockingQueue;

static class LinkedBlockingQueueTest {
    public static void main(String[] args) {
        LinkedBlockingQueue queue = new LinkedBlockingQueue();
        queue.offer("Hello");
        queue.offer("Java");
        queue.offer("world");
        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
    }
}
/***
结果
Hello
java
world
*/
2.双端队列

双端队列(Deque)是指队列的头部和尾部都可以同时入队和出队的数据结构,如下图所示:

接下来我们来演示一下双端队列 LinkedBlockingDeque 的使用:

//双端对列
static class LinkedBlockingDequeTest {
    public static void main(String[] args) {
        // 创建一个双端队列
        LinkedBlockingDeque deque = new LinkedBlockingDeque();
        deque.offer("offer"); // 插入首个元素
        deque.offerFirst("offerFirst"); // 队头插入元素
        deque.offerLast("offerLast"); // 队尾插入元素
        while (!deque.isEmpty()) {
            // 从头遍历打印
            System.out.println(deque.poll());
        }
    }
}

/***
结果
offerFirst
offer
offerLast
*/
3.优先对列

优先队列(PriorityQueue)是一种特殊的队列,它并不是先进先出的,而是优先级高的元素先出队。

优先队列是根据二叉堆实现的,二叉堆的数据结构如下图所示:

两种类型:一种是最大堆一种是最小堆。以上展示的是最大堆,在最大堆中,任意一个父节点的值都大于等于它左右子节点的值。因为优先队列是基于二叉堆实现的,因此它可以将优先级最好的元素先出队。

优先队列的使用:

import java.util.PriorityQueue;

public class PriorityQueueTest {
    // 自定义的实体类
    static class Viper {
        private int id; // id
        private String name; // 名称
        private int level; // 等级

        public Viper(int id, String name, int level) {
            this.id = id;
            this.name = name;
            this.level = level;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getLevel() {
            return level;
        }

        public void setLevel(int level) {
            this.level = level;
        }
    }
    public static void main(String[] args) {
		PriorityQueue queue = new PriorityQueue(10, new Comparator<Viper>() {
            @Override
            public int compare(Viper v1, Viper v2) {
                // 设置优先级规则(倒序,等级越高权限越大)
                return v2.getLevel() - v1.getLevel();
            }
        });
        // 构建实体类
        Viper v1 = new Viper(1, "Java", 1);
        Viper v2 = new Viper(2, "MySQL", 5);
        Viper v3 = new Viper(3, "Redis", 3);
        // 入列
        queue.offer(v1);
        queue.offer(v2);
        queue.offer(v3);
        while (!queue.isEmpty()) {
            // 遍历名称
            Viper item = (Viper) queue.poll();
            System.out.println("Name:" + item.getName() +
                               " Level:" + item.getLevel());
        }
    }
}

/***
结果
Name:MySQL Level:5

Name:Redis Level:3

Name:Java Level:1
*/
4.延迟对列

延迟队列(DelayQueue)是基于优先队列 PriorityQueue 实现的,它可以看作是一种以时间为度量单位的优先的队列,当入队的元素到达指定的延迟时间之后方可出队。

代码演示:

public class CustomDelayQueue {
    // 延迟消息队列
    private static DelayQueue delayQueue = new DelayQueue();
    public static void main(String[] args) throws InterruptedException {
        producer(); // 调用生产者
        consumer(); // 调用消费者
    }

    // 生产者
    public static void producer() {
        // 添加消息
        delayQueue.put(new MyDelay(1000, "消息1"));
        delayQueue.put(new MyDelay(3000, "消息2"));
    }

    // 消费者
    public static void consumer() throws InterruptedException {
        System.out.println("开始执行时间:" +
                DateFormat.getDateTimeInstance().format(new Date()));
        while (!delayQueue.isEmpty()) {
            System.out.println(delayQueue.take());
        }
        System.out.println("结束执行时间:" +
                DateFormat.getDateTimeInstance().format(new Date()));
    }

    static class MyDelay implements Delayed {
        // 延迟截止时间(单位:毫秒)
        long delayTime = System.currentTimeMillis();
        // 借助 lombok 实现
        @Getter
        @Setter
        private String msg;

        /**
         * 初始化
         * @param delayTime 设置延迟执行时间
         * @param msg       执行的消息
         */
        public MyDelay(long delayTime, String msg) {
            this.delayTime = (this.delayTime + delayTime);
            this.msg = msg;
        }

        // 获取剩余时间
        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        // 队列里元素的排序依据
        @Override
        public int compareTo(Delayed o) {
            if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
                return 1;
            } else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
                return -1;
            } else {
                return 0;
            }
        }
        @Override
        public String toString() {
            return this.msg;
        }
    }
    
  /**
  结果:
  
开始执行时间:20223-02-28 20:17:28
消息1
消息2
结束执行时间:20223-02-28 20:17:31
  **/  

从上述结束执行时间和开始执行时间可以看出,消息 1 和消息 2 都正常实现了延迟执行的功能 。

5.其他对列

在 Java 的队列中有一个比较特殊的队列 SynchronousQueue,它的特别之处在于它内部没有容器,每次进行 put() 数据后(添加数据),必须等待另一个线程拿走数据后才可以再次添加数据,它的使用示例如下:

import java.util.concurrent.SynchronousQueue;

public class SynchronousQueueTest {

    public static void main(String[] args) {
        SynchronousQueue queue = new SynchronousQueue();

        // 入队
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    System.out.println(new Date() + ",元素入队");
                    queue.put("Data " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }).start();

        // 出队
        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                    System.out.println(new Date() + ",元素出队:" + queue.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

/***
Tue Feb 28 17:53:40 CST 2023,元素入队
Tue Feb 28 17:53:41 CST 2023,元素入队
Tue Feb 28 17:53:41 CST 2023,元素出队:Data 0
Tue Feb 28 17:53:42 CST 2023,元素出队:Data 1
Tue Feb 28 17:53:42 CST 2023,元素入队
Tue Feb 28 17:53:43 CST 2023,元素出队:Data 2                            
*/

从上述结果可以看出,当有一个元素入队之后,只有等到另一个线程将元素出队之后,新的元素才能再次入队。

参考链接

作者:Java中文社群

链接:https://juejin.cn/post/6886620580965089288

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java消息队列(Message Queue)是一种异步通信机制,用于在组件之间传递消息。它可以缓存和传递消息,从而实现解耦和异步处理。在Java中,有很多消息队列的实现,比如ActiveMQ、RabbitMQ、Kafka等。 下面是Java消息队列使用详解: 1. 创建消息队列 首先,需要创建一个消息队列。这可以通过调用相应消息队列的API来实现。例如,在ActiveMQ中,可以通过如下代码创建一个队列: ``` ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616"); Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue("myQueue"); ``` 2. 发送消息 要发送消息到队列中,可以使用生产者(Producer)。生产者可以将消息发送到队列中,如下所示: ``` MessageProducer producer = session.createProducer(queue); TextMessage message = session.createTextMessage("Hello World!"); producer.send(message); ``` 3. 接收消息 要从队列中接收消息,可以使用消费者(Consumer)。消费者可以从队列中接收消息,如下所示: ``` MessageConsumer consumer = session.createConsumer(queue); Message message = consumer.receive(); if (message instanceof TextMessage) { TextMessage textMessage = (TextMessage) message; String text = textMessage.getText(); System.out.println("Received message: " + text); } ``` 4. 消息监听 如果需要持续接收消息,可以使用消息监听器(MessageListener)。消息监听器可以在有新消息到达队列时自动调用,如下所示: ``` MessageConsumer consumer = session.createConsumer(queue); consumer.setMessageListener(new MessageListener() { public void onMessage(Message message) { if (message instanceof TextMessage) { TextMessage textMessage = (TextMessage) message; String text = textMessage.getText(); System.out.println("Received message: " + text); } } }); ``` 5. 事务处理 如果需要确保消息被成功接收,可以使用事务(Transaction)。事务可以确保消息被成功处理,如下所示: ``` Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); MessageProducer producer = session.createProducer(queue); TextMessage message = session.createTextMessage("Hello World!"); producer.send(message); session.commit(); ``` 以上是Java消息队列使用详解。通过消息队列,可以实现组件之间的异步通信和解耦,从而提高系统的可靠性和可扩展性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值