Java中的Queue之概述

对技术还是得有敬畏之心,总觉得Queue好像没啥,其实只是没有仔细去了解过。不过自从上次认真地看了线程池的源代码之后,发现Queue是一个很神奇的集合类。Queue的形式有无界、有界,还有堵塞、非堵塞。初略想想,这个实现可能就不简单。一个问题在线程池中,自定义线程池时,放入什么样的队列可以让线程数达

对技术还是得有敬畏之心,总觉得Queue好像没啥,其实只是没有仔细去了解过。

不过自从上次认真地看了线程池的源代码之后,发现Queue是一个很神奇的集合类。Queue的形式有无界、有界,还有堵塞、非堵塞。初略想想,这个实现可能就不简单。

一个问题

在线程池中,自定义线程池时,放入什么样的队列可以让线程数达不到maximumPoolSize

按照线程池的源码,线程增长有两个阶段:一个阶段是在达到corePoolSize之前,第二个阶段是在达到corePoolSize之后并且阻塞队列已经满了,才会继续增加线程,直到maximumPoolSize

所以解决方案有如下两种:

①如果队列是无界的,那么将不会到达第二阶段的增长,也就无法到达maximumPoolSize

②还有可以让corePoolSize在足够大的值,同时限定堆大小,保证在有限的任务数下,引发内存不足也算是一种解决方案。

在解决方案①的基础上,我给出了LinkedBlockingQueue这个答案,但是它是一个“有界”的堵塞队列,只是它的capacity默认是Integer.MAX_VALUE。因为一开始我得知它是一个无界队列,因为未做详细、深入的了解,但是后面我被指正后,引发了我对Queue的思考,因为不常用,所以了解的确不是很多;同时,Queue不仅仅是一个FIFO,还有同步操作在里面,就是生产者-消费者的同步问题的一个解决方案。综上,这让我对堵塞队列产生了强烈的兴趣。

概览

找了一下BlockingQueue大致的实现类,它们的类图如下。

从中可以看出,它们都继承自AbstractQueue,并且都实现了BlockingQueue接口,只有LinkedTransferQueue还实现了TransferQueue。加上对Queue的一些操作并不是特别熟悉,比如offer、peek、poll之类,所以从Queue接口开始,逐个了解其大致的功能。

Queue

这个接口定义了6个方法,功能分成3个,分别是插入删除获取队首元素,每个功能有2种实现,对应如下表中的关系。这两种实现的区别也很明显

Throws exception Returns special value
Insert add(e) offer(e)
Remove remove() poll()
Examine element() peek()
  • add(e)offer(e):添加失败时,add抛异常,offer返回false。
  • remove()poll() :没有元素时,remove抛异常,poll返回null。
  • element()peek():没有元素时,remove抛异常,peek返回null。

还有一点需要注意,虽然有的Queue的实现(LinkedList)允许插入null值,但是Queue通常不允许插入null值,因为null对peek、poll来说,是queue中没有元素。

BlockingQueue

继承自Queue接口,同时在Queue的基础上,新增了堵塞插入、堵塞获取并删除、可堵塞一定时长的插入、可堵塞一定时长的获取并删除。相应关系对应下表。

Throws exception Special value Blocks Times out
Insert add(e) offer(e) put(e) offer(e, time, unit)
Remove remove() poll() take() poll(time, unit)
Examine element() peek() not applicable not applicable

BlockingQueue设计主要被用来处理生产者-消费者问题。还有一点需要注意BlockingQueue不接受null元素。插入操作happen-before读取、移除。

AbstractQueue

抽象类,继承自AbstractCollection,并实现了Queue接口,但未给出具体实现,相当于引入Queue相应方法,在自身类中使用,然后靠子类实现Queue的相关方法。

它实现的add、remove、element方法,分别依靠offer、poll、peek,具体如下:

public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");
}

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

public E element() {
    E x = peek();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}

public void clear() {
    while (poll() != null)
        ;
}

正好对应于上面Queue方法的区别。

常见实现类

Java中线程安全的内置队列如下表所示。队列的底层一般分成三种:数组链表。其中,堆一般情况下是为了实现带有优先级特性的队列。

队列 有界性 数据结构
ArrayBlockingQueue bounded 加锁 arraylist
LinkedBlockingQueue optionally-bounded 加锁 linkedlist
ConcurrentLinkedQueue unbounded 无锁 linkedlist
LinkedTransferQueue unbounded 无锁 linkedlist
PriorityBlockingQueue unbounded 加锁 heap
DelayQueue unbounded 加锁 heap

基于数组线程安全的队列,比较典型的是ArrayBlockingQueue,它主要通过加锁的方式来保证线程安全;

基于链表的线程安全队列,分成LinkedBlockingQueueConcurrentLinkedQueue两大类,前者也通过锁的方式来实现线程安全,而后者以及上面表格中的LinkedTransferQueue都是通过原子变量CAS这种不加锁的方式来实现的。

通过不加锁的方式实现的队列都是无界的(无法保证队列的长度在确定的范围内);而加锁的方式,可以实现有界队列

在稳定性要求特别高的系统中,为了防止生产者速度过快,导致内存溢出,只能选择有界队列;同时,为了减少Java的垃圾回收对系统性能的影响,会尽量选择array/heap格式的数据结构。

Reference:

https://tech.meituan.com/2016/11/18/disruptor.html

https://www.cnblogs.com/WangHaiMing/p/8798709.html

https://www.cnblogs.com/stateis0/p/9062076.html

Read more

Volcano 与 Kubernetes GPU 调度学习笔记

本笔记系统整理 Volcano 调度器、Kubernetes 调度框架、GPU Device Plugin、HAMi 等云原生 AI 调度领域的核心知识,适合用于学习、复习和工程实践参考。 目录 * 第一部分:Volcano 入门 * 1. Volcano 是什么 * 2. 安装与快速使用 * 3. 核心特性一览 * 第二部分:Volcano 整体架构 * 4. Volcano 解决的核心问题 * 5. 整体架构与数据流 * 6. 三层抽象模型 * 第三部分:Volcano 核心实现原理 * 7. Session 机制 * 8. Gang Scheduling 实现 * 9. Queue 与 DRF 公平调度

容器镜像(4):镜像的常用工具箱

容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。 一、skopeo:不依赖 Daemon 的镜像瑞士军刀 skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。 1.1 安装 # Ubuntu / Debian sudo apt install -y skopeo skopeo --version # skopeo version 1.15.1 1.2 inspect:免拉取检查镜像元数据 docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd