Java 并发编程:解析多种队列类型的用途 Queue Nice !!!

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Java 并发编程:解析多种队列类型的用途 Queue Nice !!!

前言


Java 中的队列有很多,例如:ArrayBlockingQueueLinkedBlockingQueuePriorityQueueDelayQueueSynchronousQueue 等,那它们的作用是什么?又是如何分类的呢?


其实 Java 中的这些队列可以从不同的维度进行分类,例如可以从阻塞和非阻塞进行分类,也可以从有界和无界进行分类,而本文将从队列的功能上进行分类,例如:优先队列、普通队列、双端队列、延迟队列等。


虽然本文的重点是从功能上对队列进行解读,但其它分类也是 Java 中的重要概念,所以我们先来了解一下它们。


阻塞队列和非阻塞队列


阻塞队列(Blocking Queue)提供了可阻塞的 put 和 take 方法,它们与可定时的 offer 和 poll 是等价的。如果队列满了 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(); // 出列
                        System.out.println("出列");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}


以上代码的执行结果如下:

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




非阻塞队列


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

非阻塞队列的典型代表是 ConcurrentLinkedQueuePriorityQueue




有界队列和无界队列


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

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




按功能分类


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



1.普通队列

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

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




常用方法


普通队列中的常用方法有以下这些:


  • offer():添加元素,如果队列已满直接返回 false,队列未满则直接插入并返回 true;
  • poll():删除并返回队头元素,当队列为空返回 null;
  • add():添加元素,此方法是对 offer 方法的简单封装,如果队列已满,抛出 IllegalStateException 异常;
  • remove():直接删除队头元素;
  • put():添加元素,如果队列已经满,则会阻塞等待插入;
  • take():删除并返回队头元素,当队列为空,则会阻塞等待;
  • peek():查询队头元素,但不会进行删除;
  • element():对 peek 方法进行简单封装,如果队头元素存在则取出并不删除,如果不存在抛出 NoSuchElementException 异常。

注意:一般情况下 offer() 和 poll() 方法配合使用,put() 和 take() 阻塞方法配合使用,add() 和 remove() 方法会配合使用,程序中常用的是 offer() 和 poll() 方法,因此这两个方法比较友好,不会报错


接下来我们以 LinkedBlockingQueue 为例,演示一下普通队列的使用:

public class LinkedBlockingQueueTest {
    public static void main(String[] args) {
        LinkedBlockingQueue queue = new LinkedBlockingQueue();
        queue.offer("Hello");
        queue.offer("Java");
        queue.offer("Myxq");
        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
    }
}




2.双端队列


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

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

public 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());
        }
    }
}

以上代码的执行结果如下:




3.优先队列


优先队列(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, "张三", 1);
            Viper v2 = new Viper(2, "李四", 5);
            Viper v3 = new Viper(3, "王五", 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());
            }
        }
}


以上代码的执行结果如下:

从上述结果可以看出,优先队列的出队是不考虑入队顺序的,它始终遵循的是优先级高的元素先出队




4.延迟队列


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

我们来演示一下延迟队列的使用:

public class CustomDelayQueue {
    // 延迟消息队列
    private static DelayQueue delayQueue = new DelayQueue();
    static class MyDelay implements Delayed {
        // 延迟截止时间(单位:毫秒)
        long delayTime = System.currentTimeMillis();
        private String msg;
        public String getMsg() {
            return msg;
        }
        public void setMsg(String msg) {
            this.msg = 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;
        }
    }
    // 生产者
    public static void producer() {
        // 添加消息
        delayQueue.put(new MyDelay(1000, "消息1"));
        delayQueue.put(new MyDelay(3000, "消息2"));
    }
    public static void main(String[] args) throws InterruptedException {
        producer(); // 调用生产者
        consumer(); // 调用消费者
    }
    // 消费者
    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()));
    }
}

以上代码的执行结果如下:

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



5.其他队列


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

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(DateFormat.getDateTimeInstance().format(new Date())+" Data " + i+"元素入队");
                    queue.put("Data " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        // 出队
        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                    System.out.println(DateFormat.getDateTimeInstance().format(new Date())+" "+ queue.take()+"元素出队");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

以上代码的执行结果如下:

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



总结


本文讲了 Java 中的 5 种队列:普通队列、双端队列、优先队列、延迟队列、其他队列。其中普通队列的典型代表为 ArrayBlockingQueue 和 LinkedBlockingQueue

双端队列的代表为 LinkedBlockingDeque

优先队列的代表为 PriorityQueue

延迟队列的代表为 DelayQueue

内部没有容器的其他队列 SynchronousQueue




最后


本期结束咱们下次再见👋~

🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗

相关文章
|
1月前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
11天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
15天前
|
算法 Java 调度
java并发编程中Monitor里的waitSet和EntryList都是做什么的
在Java并发编程中,Monitor内部包含两个重要队列:等待集(Wait Set)和入口列表(Entry List)。Wait Set用于线程的条件等待和协作,线程调用`wait()`后进入此集合,通过`notify()`或`notifyAll()`唤醒。Entry List则管理锁的竞争,未能获取锁的线程在此排队,等待锁释放后重新竞争。理解两者区别有助于设计高效的多线程程序。 - **Wait Set**:线程调用`wait()`后进入,等待条件满足被唤醒,需重新竞争锁。 - **Entry List**:多个线程竞争锁时,未获锁的线程在此排队,等待锁释放后获取锁继续执行。
49 12
|
11天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
93 2
|
28天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
1月前
|
存储 编译器 C语言
【C语言】数据类型全解析:编程效率提升的秘诀
在C语言中,合理选择和使用数据类型是编程的关键。通过深入理解基本数据类型和派生数据类型,掌握类型限定符和扩展技巧,可以编写出高效、稳定、可维护的代码。无论是在普通应用还是嵌入式系统中,数据类型的合理使用都能显著提升程序的性能和可靠性。
46 8
|
28天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
50 3
|
1月前
|
算法 调度 开发者
多线程编程核心:上下文切换深度解析
在多线程编程中,上下文切换是一个至关重要的概念,它直接影响到程序的性能和响应速度。本文将深入探讨上下文切换的含义、原因、影响以及如何优化,帮助你在工作和学习中更好地理解和应用多线程技术。
40 4
|
存储 算法 安全
【Java 数据结构及算法实战】系列 014:Java队列08——数组实现的双端队列ArrayDeque
【Java 数据结构及算法实战】系列 014:Java队列08——数组实现的双端队列ArrayDeque
174 0
【Java 数据结构及算法实战】系列 014:Java队列08——数组实现的双端队列ArrayDeque
|
存储 算法 安全
【Java数据结构及算法实战】系列012:Java队列06——数组实现的优先级阻塞队列PriorityBlockingQueue
【Java数据结构及算法实战】系列012:Java队列06——数组实现的优先级阻塞队列PriorityBlockingQueue
144 0

热门文章

最新文章

推荐镜像

更多