深入了解 LinkedBlockingQueue阻塞队列,分析扩容机制以及小顶堆原理

news2025/1/15 13:01:10

1. 前言

今天的目的是为了深入了解下优先队列的机制。不过优先队列是基于大小顶堆实现的,但是其本质就是一个二叉树,所以今天会讲一些铺垫知识,好了,废话不多说了,让我们开始吧

2. 前置知识

2.1 大顶堆

大顶堆

  • 完全二叉树:

    • 一棵深度为 k 的有 n 个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为 i(1≤i≤n)的结点与满二叉树中编号为 i 的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树
    • 一棵深度为 k 且有个结点的二叉树称为满二叉树
    • 叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部
    • 如果完全二叉树缺少节点,那一定在右侧
      完全二叉树
  • 大顶堆:

    • 必须是一个完全二叉树(满足二叉树的所有的性质)
    • 任何子树中必须满足父元素大于任何子元素的值
  • 需要实现的方法:

    • peek(): T | boolean 查询堆顶的数据,但是不修改数据
    • poll(): T | boolean 从堆顶弹出一个元素,调整结构(堆尾元素添加到堆顶,并且开始下调整)
    • offer(value: T): boolean 从堆底添加一个元素,调整结构(上调整)
    • isEmpty(): boolean 判断堆是否为空
    • size(): number 返回堆的长度
  • 实现案例

    • 求最值 场景使用最多(比如:获取前 5 个最小值)

2.2 小顶堆

小顶堆

  • 完全二叉树:

    • 一棵深度为 k 的有 n 个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为 i(1≤i≤n)的结点与满二叉树中编号为 i 的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树
    • 一棵深度为 k 且有个结点的二叉树称为满二叉树
    • 叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部
    • 如果完全二叉树缺少节点,那一定在右侧
      完全二叉树
  • 小顶堆:

    • 必须是一个完全二叉树(满足二叉树的所有的性质)
    • 任何子树中必须满足父元素小于任何子元素的值
  • 需要实现的方法:

    • peek(): T | boolean 查询堆顶的数据,但是不修改数据
    • poll(): T | boolean 从堆顶弹出一个元素,调整结构(堆尾元素添加到堆顶,并且开始下调整)
    • offer(value: T): boolean 从堆底添加一个元素,调整结构(上调整)
    • isEmpty(): boolean 判断堆是否为空
    • size(): number 返回堆的长度
  • 实现案例

    • 求最值 场景使用最多(比如:获取前 5 个最大值)

3. 优先队列

3.1 构造函数

既然是优先队列,就需要通过比较大小来决定谁先输出。被添加的元素要实现了Comparator接口,可以进行比较.

在这里插入图片描述

3.1.1 基本构造属性

// 数组的默认长度。 虽然是一个完全二叉树,但是底层是基于数组来实现的
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 表示数组的最大值。 减8的目的是为了兼容不同的JDK版本
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 存储元素的地方
private transient Object[] queue;
// 用来表示队里的长度
private transient int size;
// 此处是比较器
private transient Comparator<? super E> comparator;
// 此处表示一个锁
private final ReentrantLock lock;
// 表示消费者挂起condition
private final Condition notEmpty;
// 目的是为了避免并发扩容
private transient volatile int allocationSpinLock;

3.1.2 构造函数实现

public PriorityBlockingQueue(int initialCapacity,
                             Comparator<? super E> comparator) {
    // 如果长度小于1的话 直接报错
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    // 实例化锁
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    // 表示比较器
    this.comparator = comparator;
    // 表示存储对象的数组
    this.queue = new Object[initialCapacity];
}

3.2 生产者方法

3.2.1 add

public boolean add(E e) {
    // add方法 本质上就是调用offer方法
    return offer(e);
}

3.2.2 offer

public boolean offer(E e) {
    // 添加的元素不能为null  如果为null的话 直接报错
    if (e == null)
        throw new NullPointerException();
    // 局部变量 获取锁实例
    final ReentrantLock lock = this.lock;
    // 上锁
    lock.lock();
    int n, cap;
    Object[] array;
    // queue 表示的数组本身
    // size 优先级中队列元素
    // 如果优先队列中元素 >= 数组的长度的话 直接选择扩容
    while ((n = size) >= (cap = (array = queue).length))
        // 扩容方法
        tryGrow(array, cap);
    try {
        // 执行到此时 表示要么扩容成功  要么就不满足while条件
        Comparator<? super E> cmp = comparator;
        // 如果cmp 比较器 为null的话
        if (cmp == null)
            // n 表示size长度
            // e 表示添加的元素
            // array 优先级队列本身
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        // 表示size 累加
        size = n + 1;
        // 通过消费者 开始消费。 因为有可能之前队列是空的 消费者消费时导致线程挂起
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

3.2.3 小顶推实现

  1. 如果想实现小顶堆,应该牢记小顶堆的原理。任意一个元素,都比其子元素要小
  2. 如果root 节点应该是最小的值
  3. 为了维护这种关系,我们应该向上查找,直到找到比当前值小的值为止
private static <T> void siftUpComparable(int k, T x, Object[] array) {
    // 此时表示添加的元素 。元素实现了Comparable 比较器
    Comparable<? super T> key = (Comparable<? super T>) x;
    // 此while循环是一个小顶堆实现原理
    while (k > 0) {
        // 虽然小顶堆是完全二叉树,但是存放顺序可以放到数组中。 如果是子元素寻找父亲的话可以是`(k - 1) >>> 1`
        int parent = (k - 1) >>> 1;
        // 找到父亲的位置 获取对应的值
        Object e = array[parent];
        // 将当前元素 跟父类元素比较  应该是 当前的值 -  父亲的值 >= 0. 因为是小顶堆 所以应该向上找比当前值 更小的值
        if (key.compareTo((T) e) >= 0)
            break;
        // 移动父亲的位置
        array[k] = e;
        // 当前parent 成为新的key
        k = parent;
    }
    // 设置最小的值
    array[k] = key;
}

3.2.4 扩容数组实现原理

  1. 这里简单简述下 整个数组扩容的思想:
  2. 如果原来的数组长度 < 64 的话,设置新的长度是 旧长度 * 2 + 2
  3. 如果原来的数组长度 >= 64的话,设置的新的长度是 旧长度 * 1.5
  4. 然后比较是否超过最大长度等等 做一系列长度
// 此方法是一个数组扩容方法
// oldCap 原数组队列的长度
// array 是一个原数组
private void tryGrow(Object[] array, int oldCap) {
    // 解锁
    lock.unlock();
    // 新的数组
    Object[] newArray = null;
    // allocationSpinLock 为 0的时候 表示目前没有线程 处于一个扩容的状态
    if (allocationSpinLock == 0 &&
        // 通过CAS 进行allocationSpinLock值设置。 保证了线程安全。
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                 0, 1)) {
        // 如果数组的长度 < 64 的话 实际长度就是oldCap * 2 + 2 反之 oldCap * 1.5
        try {
            int newCap = oldCap + ((oldCap < 64) ?
                                   (oldCap + 2) : // grow faster if small
                                   (oldCap >> 1));
            // 如果设置的新的长度 比 最大值还大
            if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                // 扩容最小长度  = 旧数组长度 + 1
                int minCap = oldCap + 1;
                // 如果为负值 或是 最小都比MAX_ARRAY_SIZE 大的话 直接就是溢出异常
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
                // 将新数组长度 设置为  MAX_ARRAY_SIZE
                newCap = MAX_ARRAY_SIZE;
            }
            // 设置新的长度 > 原来的长度  && 数组没有变化
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];
        } finally {
            allocationSpinLock = 0;
        }
    }
    if (newArray == null) // back off if another thread is allocating
        Thread.yield();
    lock.lock();
    // 新的数组 以及被实例化了 && 原数组本身没有变化
    if (newArray != null && queue == array) {
        // 进行值copy
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

3.2.5 有参offer

public boolean offer(E e, long timeout, TimeUnit unit) {
    // 可以理解为优先级队列是无界的 因为一直在扩容。 所以不会存在队满的情况,所以不需要等待
    return offer(e); // never need to block
}

3.2.6 put

public void put(E e) {
    // 此处表示直接添加 绝不会blocking住
    offer(e); // never need to block
}

3.3 消费者方法

3.3.1 remove

public E remove() {
    // 表示删除一个元素
    E x = poll();
    // 如果删除的元素不为null的话 直接返回值
    if (x != null)
        return x;
    else
        // 如果为null的话 直接抛出异常
        throw new NoSuchElementException();
}

3.3.2 poll

public E poll() {
    // 获取锁实例
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 返回提出的值
        return dequeue();
    } finally {
        // 解锁
        lock.unlock();
    }
}

// 此方法是获取堆顶的元素
private E dequeue() {
    // 表示-1 后的长度
    int n = size - 1;
    // 如果是<0的话 表示是空队列
    if (n < 0)
        return null;
    else {
        // 如果执行到此处表示不是空队列
        Object[] array = queue;
        // 获取堆顶元素
        E result = (E) array[0];
        // 获取最后一个元素
        E x = (E) array[n];
        // 最后一个元素重置为null
        array[n] = null;
        // 比较器
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            // 元素下沉
            siftDownComparable(0, x, array, n);
        else
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}

3.3.3 维护堆,下沉方法

// 为了维护堆中元素 实现下沉的方法
// k 此时的值 为 0
// x 原来堆中最后一个元素 其实就是最大元素
// array 表示队列中的原数组
// n 队列中长度 - 1
private static <T> void siftDownComparable(int k, T x, Object[] array,
                                           int n) {

    // 如果n<= 0 表示 此时队列中没有数据 所以没有必要下沉
    if (n > 0) {
        // 此时元素 实现了Comparable 比较器
        Comparable<? super T> key = (Comparable<? super T>)x;
        // 一半
        int half = n >>> 1;           // loop while a non-leaf
        while (k < half) {
            // 左子元素
            int child = (k << 1) + 1; // assume left child is least
            // 左子元素值
            Object c = array[child];
            // 右子元素下标
            int right = child + 1;
            // 左右子树进行比较 如果左子树 > 右子树
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                c = array[child = right];
            // 如果父元素 已经比子树小了 就到头了
            if (key.compareTo((T) c) <= 0)
                break;
            // 重新设置元素 以及下标
            array[k] = c;
            k = child;
        }
        array[k] = key;
    }
}

3.3.4 有参poll

// 表示延迟时间 timeout
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    // 格式化时间 统一转换为纳秒
    long nanos = unit.toNanos(timeout);
    // 获取锁实例
    final ReentrantLock lock = this.lock;
    // 上锁
    lock.lockInterruptibly();
    E result;
    try {
        // 队列中没有数据 && 还有等待时间
        while ( (result = dequeue()) == null && nanos > 0)
            // 暂时挂起线程
            nanos = notEmpty.awaitNanos(nanos);
    } finally {
        lock.unlock();
    }
    return result;
}

3.3.5 take

public E take() throws InterruptedException {
    // 获取锁实例
    final ReentrantLock lock = this.lock;
    // 上锁 可被打断锁
    lock.lockInterruptibly();
    E result;
    try {
        // 如果队列中没有数据 然后挂起线程 反之就是一直等待
        while ( (result = dequeue()) == null)
            notEmpty.await();
    } finally {
        lock.unlock();
    }
    return result;
}

4. 结束

代码就分析到这里了。其实核心内容就是:扩容机制 和 通过小顶堆 如何维护队列的数据结构。 上述的代码中每一行代码都标注了注释,如果大家还有什么疑问的话,欢迎及时留言区评论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/162593.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1.2、操作系统的特征

1、并发 并发\color{red}并发并发&#xff1a;指两个或多个事件在同一时间间隔内发生。 这些事件宏观上是同时发生\color{red}宏观上是同时发生宏观上是同时发生的&#xff0c;但微观上是交替发生\color{red}微观上是交替发生微观上是交替发生的。 并行\color{red}并行并行&am…

STM32F103学习笔记(10)——I2C多路复用器TCA9548A使用

一、简介 TCA9548A 器件配有八个可通过 I2C 总线控制的双向转换开关。串行时钟/串行数据 (SCL/SDA) 上行对可扩展为 8 个下行对或通道。根据可编程控制寄存器的内容&#xff0c;可选择任一单独 SCn/SDn 通道或者通道组合。这些下游通道可用于解决 I2C 从器件地址冲突。例如&…

高精度加法【c++实现】超详细讲解

高精度存在的意义 大家一定都知道int和long long是有极限的&#xff08;如下表&#xff09;&#xff0c;如果超了就无法计算正确结果了&#xff0c;那该用什么方法来计算呢&#xff1f;这就是我们今天要说的算法———高精度算法。&#xff08;本文只讲加法&#xff09; 类型…

超级浏览器的防关联效果怎么样?

很多从事跨境电商业务的朋友&#xff0c;都尝试用各种手段来防止账号关联&#xff0c;现在有很多不要钱的超级浏览器可以下载使用&#xff0c;但是很多人却不敢把高价值的账号放在超级浏览器上面&#xff0c;今天我们就来详细聊聊这个问题。说超级浏览器之前&#xff0c;我们先…

抖音世界杯直播的低延迟是怎么做到的?

动手点关注干货不迷路世界杯已经结束了&#xff0c;梅西带领阿根廷时隔三十六年之后终于如愿捧杯。抖音直播提供的 4K 超高清超低延迟看播能力给亿万观众留下了深刻的印象&#xff0c;决赛的 PCU 达到 3700w&#xff0c;在这样大规模并发下&#xff0c;如何能稳定流畅地做到更低…

GO语言配置和基础语法应用(一)

一、golang的下载和安装 这一步比较简单&#xff0c;直接打开go的官网&#xff0c;点击download即可&#xff0c;个别人打开慢可以用中国的镜像网站&#xff0c;之后访问的速度和下载第三方库的速度会快很多&#xff0c;之后傻瓜式安装一路到底即可。 配置环境变量 注意&#…

经典文献阅读之--Multi-modal Semantic SLAM(多模态语义SLAM)

0. 简介 在复杂动态环境下&#xff0c;如何去建立一个稳定的SLAM地图是至关重要的。但是现在当前的SLAM系统主要是面向静态场景。目前相较于点云的分类与分割而言。视觉的识别与分割会更加容易。这就可以根据语义信息提高对环境的理解。文章《Multi-modal Semantic SLAM for C…

JavaScript 如何正确的读懂报错信息

文章目录前言一、查看报错1.控制台报错2.终端报错二、查找错误演示总结前言 一、查看报错 如何阅读报错信息, 根据信息快速锁定错误. 1.控制台报错 红色报错信息格式: xxxx Error: xxxxx报错信息xxxxx 最终报错文件:行编号 at 最终报错方法名 (最终报错文…

PySpark中RDD的数据输出详解

目录 一. 回顾 二.输出为python对象 collect算子 演示 reduce算子 演示 take算子 演示 count算子 演示 小结 三.输出到文件中 savaAsTextFile算子 演示 配置Hadoop依赖 修改rdd分区为1个 小结 四.练习案例 需求&#xff1a; 代码 一. 回顾 数据输入: sc.paralle…

Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context_学习笔记

Transformer-XL学习笔记 一、Transformer-XL出现的原因 首先说明Transformer的变形版本Transformer-XL出现的原因&#xff1a; transformer作为一种特提取器&#xff0c;在NLP中有广泛的应用&#xff0c;但是transformer需要对输入的序列设置固定的长度&#xff0c;例如在Ber…

(考研湖科大教书匠计算机网络)第一章概述-第四节:计算机网络的性能指标

文章目录&#xff08;1&#xff09;速率&#xff08;2&#xff09;带宽&#xff08;3&#xff09;吞吐量&#xff08;4&#xff09;时延①&#xff1a;基本概念②&#xff1a;计算公式&#xff08;5&#xff09;时延带宽积&#xff08;6&#xff09;往返时间RTT&#xff08;7&a…

dp(六) 线性dp整合 最长(公共子串、公共子序列、上升子序列、回文子串)

1、最大公共子串_牛客题霸_牛客网​编辑 2、最长上升子序列(一)_牛客题霸_牛客网 3、最长回文子串_牛客题霸_牛客网 4、最长公共子序列(二)_牛客题霸_牛客网 #include <iostream> using namespace std; #include<vector>int main() {string str1,str2;cin>>…

mysql数据迁移报错问题

mysql8.0.17备份数据库到mysql5.7.26的There was error(s) while executing the queries问题解决&#xff08;数据库高版本向低版本数据迁移解决&#xff09; 问题背景 要将本地的mysql数据库导入到linux中的mysql中&#xff0c;其中&#xff0c;本地mysql数据库的版本是8.0.…

数字硬件建模SystemVerilog-时序逻辑建模(1)RTL时序逻辑的综合要求

数字门级电路可分为两大类&#xff1a;组合逻辑和时序逻辑。锁存器是组合逻辑和时序逻辑的一个交叉点&#xff0c;在后面会作为单独的主题处理。组合逻辑描述了门级电路&#xff0c;其中逻辑块的输出直接反映到该块的输入值的组合&#xff0c;例如&#xff0c;双输入AND门的输出…

N5247A网络分析仪

18320918653 N5247A Agilent N5247A 网络分析仪主要特性与技术指标 10 MHz 至 67 GHz2 端口或 4 端口&#xff0c;具有两个内置信号源可提供 4 端口 110 GHz 单次扫描解决方案110 dB 系统动态范围&#xff0c;32001 个点&#xff0c;32 个通道&#xff0c;5 MHz 中频带宽高输…

MySQL中深入浅出索引

文章目录前言一、索引的常见模型二、InnoDB的索引模型三、索引的维护四、索引的优化覆盖索引联合索引最左前缀原则索引下推前言 我们在看书的时候&#xff0c;打算回看某一个桥段的内容时。这是你肯定会是先翻看书的目录&#xff0c;从目录确定这段内容的位置&#xff0c;然后…

爬虫利用多线程快速爬取数据

一般单线程爬数据太慢了话&#xff0c;就采用多线程。 一般要根据两种情形来选择 自定义线程线程池 往往最关键的地方在&#xff0c;多个线程并发执行后&#xff0c;是否需要线性的返回结果。也就是先调用的线程&#xff0c;返回的结果要在前面。 或者说&#xff0c;某个对…

mysql简单数据查询——数采数据电量与耗料的日统计

目录 前言 步骤1&#xff1a;date_format函数 步骤2&#xff1a;concat函数 步骤3、4&#xff1a;查询中使用变量 完整代码 前言 在数采数据已写入mysql数据库中后&#xff0c;进行数据处理&#xff0c;统计电量与耗料数据 由于数据库版本较低&#xff0c;无法使用较新的…

华为策略路由实验配置

配置接口相关的IP地址&#xff0c;并配置IGP路由协议使得全网互通 AR1配置接口策略路由 对经过本地转发的路由生效&#xff0c;对本地始发的路由不生效 配置nqa检测下一跳状态 nqa test-instance PC1 icmptrace nqa的管理者为PC1&#xff0c;NQA的测试例名为icmptrace test-…

全国青少年软件编程(Scratch)等级考试二级考试真题2022年12月——持续更新.....

1.一个骰子,从3个不同角度看过去的点数如图所示,请问5的对面是什么点数?( ) A.1 B.3 C.4 D.6 正确答案:A 答案解析: 根据图三,用右手定则,大拇指朝上指向6所对的方向,其余四指握起来表示旋转方向,可以看到先5后2,然后把这个姿势对应到图1中,就知道1的对面是5…