环形队列出队的元素怎么输出出来_队列:队列在线程池等有限资源池中的应用...

我们知道,CPU资源是有限的,任务的处理速度与线程个数并不是线性正相关的。相反,过多的线程反而会导致CPU频繁切换,处理性能下降。所以,线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,来事先设置的。

当我们向固定大小的线程池中请求一个线程时,如果线程池没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?

实际上,这些问题并不复杂,其底层的数据结构就是我们今天要学的内容,队列(queue)。

如果理解“队列”

队列这个概念非常好理解。你可以把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进来先出,这就是典型的“队列”。

我们知道,栈只支持两个基本操作:入栈push()和出栈pop()。队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:入队enqueue(),放一个数据到队列尾部;出列dequeue(),从队列头部取一个元素。

1c0904b2b3f30d62dc61aab644008add.png

所以,队列跟栈一样,也是一种操作受限的线性表数据结构。

队列的概念很好理解,基本操作也很容易掌握。作为一种非常基础的数据结构,队列的应用也非常广泛,特别是一些具有某些额外特性的队列,比如循环队列,阻塞队列,并发队列。他们在很多偏底层系统,框架,中间件的开发中,起着关键性的作用。比如高性能队列Disruptor,Linux环形缓存,都用到循环并发队列;Java concurrent并发包利用ArrayBlockingQueue来实现公平锁。

顺序队列和链式队列

我们知道了,队列和栈一样,也是一种抽象的数据结构。它具有先进先出的特性,支持在队尾插入元素,在对头删除元素,那究竟该如何实现一个队列呢?

跟栈一样,队列可以用数组来实现,也可以用链表来实现。用数组实现的栈叫顺序栈,用链表实现的栈叫链式栈。同样,用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。

我们先来看下基于数组的实现方法。我用java语言实现了一下,不过并不包含Java语言的高级语法,而且我做了比较详细的注释,你应该可以看懂。

//用数组实现的队列

比起栈的数组实现,队列的数组实现稍微有点儿复杂,但是没关系。我稍微解释一下实现思路,你很容易就能明白了。

对于栈来说,我们只需要一个栈顶指针就可以了。但是队列需要两个指针:一个是head指针,指向对头;一个是tail指针,指向队尾。

你可以结合下面这幅图来理解。当a,b,c,d依次入队之后,队列中的head指针指向下标为0的位置,tail指针指向下标为4的位置。

b8c1bddedf25febcf487caaf278ce775.png

当我们调用两次出队操作之后,队列中head指针指向下标为2的位置,tail指针仍然指向下标为4的位置。

a339b490847e65266119021f08b7f35a.png

你肯定已经发现了,随着不停地进行入队,出队操作,head和tail都会持续往后移动。当tail移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。这个问题该如何解决呢?

你是否还记得,在数组那一节,我们也遇到过类似的问题,就是数组的删除操作会导致数组中的数组不连续。你还记得我们当时是怎么解决的吗?对用数据搬移!但是,每次进行出队操作都相当于删除数组下标为0的数据,要搬移整个队列中的数据,这样出队操作的时间复杂度就会从原来的O(1)变成O(n)。能不能优化一下呢?

实际上,我们在出队时可以不用搬移数据。如果没有空闲空间了,我们只需要在入队时,再集中触发一次数据的搬移操作。借助这个思想,出队函数dequeue()保持不变,我们稍加改造一下函数enqueue()的实现,就可以轻松解决刚才的问题了。

下面是具体的代码:

//入队操作,将item放入队尾

从代码中我们看到,当队列的tail指针移动到数组的最右边后,如果有新的数据入队,我们可以将head到tail之间的数据,整体搬移到数组中0到tail-head的位置。

7d280d24679f74383ecc9c80019839c6.png

这种实现思路中,出队操作的时间复杂度仍然是O(1),但入队操作的时间复杂度还是O(1)吗?你可以用我们第3节,第4节讲的算法复杂度分析方法,自己试着分析一下。

接下来,我们再来看下基于链表的队列实现方法。

基于链表的实现,我们同样需要两个指针:head指针和tail指针。他们分别指向链表的第一个结点和最后一个结点。如图所示,入队是,tail->next=new_node,tail = tail->next;出队时,head = head->next。

ac36101958668a3338e46b0af211d38e.png

循环队列

我们刚才用数组来实现队列的时候,在tail == n时,会有数据搬移操作,这样入队操作性能就会收到影响。那有没有办法能够避免数据搬移呢?我们来看看循环队列的解决思路。

循环队列,顾名思义,它长得像一个环。原本数组是有头有尾的,是一条直线。现在我们把首尾相连,扳成了一个环。我画了一张图,你可以直观地感受一下。

c679fc73a26b2917a7a9d6514b7f31ab.png

我们可以看到,图中这个队列的大小为8,当前head=4,tail=7。当有一个新的元素a入队时,我们放入下标为7的位置。但这个时候,我们并不把tail更新为8,而是将其在环中后移一位,到下标为0的位置。当再有一个元素b入队时,我们将b放入下标为0的位置,然后tail加1更新为1。所以,在a,b依次入队之后,循环队列中的元素就变成了下面的样子:

a4170410b40dd27a0124d20ea371b7d2.png

通过这样的方法,我们成功避免了数据搬移操作。看起来不难理解,但是循环队列的代码实现难度要比前面讲的非循环队列难多了。要想写出没有bug的循环队列的实现代码,我个人觉得,最关键的是,确定好对空和队满的判定条件

在用数组实现的非循环队列中,队满的判断条件是tail ==n,队空的判断条件是head==tail。那针对循环队列,如何判断队空和队满呢?

队列为空的判断条件仍然是head == tail。但队列满的判断条件就稍微有点复杂了。我画了一张队列满的图,你可以看一下,试着总结一下规律。

a15d6fb931dabb0d6e7d12be78dec77f.png

就像我图中画的队满的情况,tail=3,head=4,n=8,所以总结一下规律就是:(3+1)%8=4。

多画几张队满 的图,你就会发现,当队满时,(tail+1)%n=head。

你有没有发现,当队列满时,图中的tail指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。

Taik is cheap,如果还是没怎么理解,那就show 有code吧。

public 

阻塞队列和并发队列

前面讲的内容理论比较多,看起来很难跟实际的项目开发扯上关系。确实,队列这种数据结构很基础,平时的业务开发不大可能从零实现一个队列,甚至都不会直接用到。而一些具有特殊特性的队列应用却比较广泛,比如阻塞队列和并发队列。

阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

2236683417a51d62d5be2917c05e2f11.png

你应该已经发现了,上述的定义就是一个“生产者-消费者模型!”是的,我们可以使用阻塞队列,轻松实现一个“生产者-消费者模型”!

这种基于阻塞队列实现的“生产者-消费者模型”,可以有效地协调生产和消费的速度。当“生产者”生产数据的速度过快,“消费者”来不及消费时,存储数据的队列很快就会满了。这个时候,生产者阻塞等待,知道“消费者”消费了数据,“生产者”才会被唤醒继续“生产”。

而且不仅如此,基于阻塞队列,我们还可以通过协调“生产者”和“消费者”的个数,来提高数据的处理效率。比如前面的例子,我们可以配置几个“消费者”,来应对一个“生产者”。

a40da30bee9c3aaf4e6e93bab928107e.png

前面我们讲了阻塞队列,在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,那如何实现一个线程安全的队列呢?

线程安全的队列我们叫作并发队列。最简单直接的实现方式是直接在enqueue(),dequeue()方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用CAS原子操作,可以实现非常高效的并发队列。这个也是循环队列比链式队列应用更加广泛的原因。在实战篇讲Disruptor的时候,我会再详细讲并发队列的应用。

解答开篇

队列的知识就讲完了,我们现在回过来看下开篇的问题。线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?各种处理策略又是如何现实的呢?

我们一般有两种处理策略。第一种是非阻塞的处理方式,直接拒绝任务请求;另一种是阻塞的处理方法,将请求排队,等到有空闲线程时,取出排队的请求继续处理。那如何存储排队的请求呢?

我们希望公平地处理每个排队的请求,先进者先服务,所以队列这种数据结构很适合来存储排队请求。我们前面说过,队列有基于链表和基于数组这两种实现方式。这两种实现方式对于排队请求又有什么区别呢?

基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue),但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的。

而基于数组实现的有界队列(bounded queue),队列的大小有限,所以线程池中队列的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应敏感的系统来说,就相对更加合理。不过,设置一个合理的队列大小,也是非常有讲究的。队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源,发挥最大性能。

除了前面讲到队列应用在线程池请求队列的场景之外,队列可以应用在任何有限资源池中,用于排队请求,比如数据库连接池等。实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。

内容小结

今天我们讲 了一种跟栈很相似的数据结构,队列。关于队列,你能掌握下面的内容,这节就没问题了。

队列最大的特点就是先进先出,主要的两个操作是入队和出队。跟栈一样,它既可以用数组来实现,也可以用链表来实现。用数组实现的叫顺序队列,用链表实现的叫链式队列。特别是长得像一个环的循环队列。在数组实现队列的时候,会有数组搬移操作,要想解决数据搬移的问题,我们就需要像环一样的循环队列。

循环队列是我们这节的重点。要想写出没有bug的循环队列实现代码,关键要确定好队空和队满的判定条件,具体的代码你要能写出来。

除此之外,我们还讲了几种高级队列结构,阻塞队列,并发队列,底层都还是队列这种数据结构,只不过在之上附加了很多其他功能。阻塞队列就是入队,出队操作可以阻塞,并发队列就是队列的操作多线程安全。

参考文献

王争老师 《数据结构与算法之美》

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

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

相关文章

英语答题测试的软件叫什么,英语做题软件哪个好 有答案解析的英语做题软件分享...

对于一些即将参与重要英语考试的考生来说,每天刷题练习肯定是不可避免的事情,但如果你想要更高效的刷题,让自己的刷题时间更有价值,那就来看看推荐给你的这些英语做题软件。类型:学习 语言:简体中文星级&am…

raid卡组不同raid_RAID磁盘阵列是如何运作的?

本文编辑:意哥专业指导:葵芳一凡 RAID是英文Redundant Array of Independent Disks的缩写,中文简称为独立冗余磁盘阵列。简单的说,RAID是一种把多块独立的硬盘(物理硬盘)按不同的方式组合起来形成一个硬盘组…

java 析构函数_C++虚函数

码字不易,欢迎给个赞!C虚函数是多态性实现的重要方式,当某个虚函数通过指针或者引用调用时,编译器产生的代码直到运行时才能确定到底调用哪个版本的函数。被调用的函数是与绑定到指针或者引用上的对象的动态类型相匹配的那个。因此…

象过河软件试用版_比肩许银川蒋川王天一,象棋软件下出神一样的残局,看完叹为观止...

古语有云,残局,是一盘棋的命脉所在。这对于现代象棋而言,也是如此,君不见,许银川有着鬼魅残功,蒋川有着魔衣血刀,王天一有着天外飞仙,都是一等一的残棋神器。而被誉为棋界战斗力最为…

python爬取小说出现乱码_详解Python解决抓取内容乱码问题(decode和encode解码)

一、乱码问题描述 经常在爬虫或者一些操作的时候,经常会出现中文乱码等问题,如下原因是源网页编码和爬取下来后的编码格式不一致 二、利用encode与decode解决乱码问题 字符串在Python内部的表示是unicode编码,在做编码转换时,通常…

北方股份无人驾驶矿卡_踏歌智行完成B轮2亿元融资,无人驾驶矿山赛道爆发在即...

作者 / 李笠10 月 30 日,矿山无人驾驶运输的领军企业踏歌智行完成了 2 亿元 B 轮融资。这是无人驾驶矿山赛道迄今为止最大的一笔融资。据悉,本轮融资由前海母基金和宝通投资共同领投,清研资本、蓝焱资本等跟投。这也是踏歌智行继 2019 年连续…

计算机视觉子方向,计算机视觉方向简介 | 人脸识别中的活体检测算法综述

原标题:计算机视觉方向简介 | 人脸识别中的活体检测算法综述本文转载自“SIGAI人工智能学习与实践平台”(ID:SIGAICN)导言1. 什么是活体检测?判断捕捉到的人脸是真实人脸,还是伪造的人脸攻击(如:彩色纸张打印人脸图&am…

黑客们的故事(连载三) 因为好奇

上期说到的肯和丹尼斯在黑客文化中的高知名度,不仅是因为他们开发出的操作系统和编程语言,更在于他们行为所体现出的价值观:求知、探索,以及追求极致。这些是黑客精神的精髓——虽然黑客们贯彻这些精神时的方式并不总是合理合法的…

css3禅密花园叫什么名字_新生儿起名:2021元旦出生男孩叫什么名字

2021年元旦节又是新的一年开始,在这辞旧迎新的日子里出生的男孩,都带有极好的寓意兆头,父母们如果能顺借此时机,取一个具有出生纪念意义的名字,那必然能让男孩的成长有着不同与众的历程。2021元旦出生男孩叫什么名字比…

10个让人很舒服的沟通技巧

转载于:https://www.cnblogs.com/yymn/p/4605237.html

idea将远程代码更新合并到本地_idea 本地调试远程服务器代码

基本原理本机和远程主机的两个 VM 之间使用 Debug 协议通过 Socket 通信,传递调试指令和调试信息。 被调试程序的远程虚拟机:作为 Debug 服务端,监听 Debug 调试指令。jdwp是Java Debug Wire Protocol的缩写。 调试程序的本地虚拟机&#xff…

安卓手机网页 字体垂直方向对齐_初学Excel办公软件字体对齐调整

今天我们依然讲解Excel办公软件的字体对齐调整,上一节我们讲过在菜单栏的上方就可以看到十个功能,然后我们根据这十个功能的调解我们所需要的文字对齐,或者是居中对齐,顶端对齐等等。还有另外一种操作方法,也是方便快捷…

英伟达_如何超越英伟达?

从AI热潮中率先获益的英伟达,其GPU参数和性能频繁出现在不少AI芯片发布会的对比图中。这是在缺乏AI芯片衡量的标准时,新的AI芯片证明自己实力的不错方式。不过,声称性能超越英伟达GPU的创新AI芯片不少,但想要超越英伟达非常困难。…

在struts2中push方法的使用_【干货】网版印刷中水墨使用注意事项及助剂使用方法...

导LEAD语在印刷生产中,水性油墨的黏度、干燥速度和PH值等等都会直接影响印刷质量,那么具体到网版印刷中,在使用水性油墨时应该注意哪些问题呢?常用的印刷助剂都有哪些特点和使用方法?编辑:华印纸箱彩盒 小张…

限定概率抽奖_守护星已点亮,内测皮肤得到没?从天美抽奖概率分析:地址什么梗...

声明:原创文章,禁止抄袭,违者必究!大家好,我是你们的小抖。本次五五开黑节最引人注意的活动,当然不是超云龙胆新皮肤了。小抖觉得大家应该与我想的一样:内测皮肤。小抖点亮了所有守护星&#xf…

完美汽配管理系统v12服务器,完美汽车维修4S店管理系统

完美汽车维修4S店管理系统是一款十分不错的汽车修理系统,他可以帮助用户去处理好汽车维修,客户信息管理等问题,所以快来下载试试吧!软件介绍完美汽车维修4S店管理系统是款界面简洁大方且主要应用汽修行业的管理软件,完…

JPA中的@MappedSuperclass

说明地址:http://docs.oracle.com/javaee/5/api/javax/persistence/MappedSuperclass.html 用来申明一个超类,继承这个类的子类映射时要映射此类中的字段,可以当做是对entity抽取封装的类。如果子类想重写此类的映射信息,可以使用…

调用另一个cpp的变量_再谈条件变量—从入门到出家

再谈条件变量—从入门到出家C语言--条件变量条件变量是在线程中以睡眠的方式等待某一条件的发生;条件变量是利用线程间共享的全局变量进行同步的一种机制:一个线程等待"条件变量的条件成立"挂起另一个线程使"条件成立"条件变量的使用…

不同page页面选择不同页面模板的方法

仿制一个企业站的时候发现该站用了很多page页面,而且个别页面的样式不相同,同时区别于post文章页面,其实不同之处就在于每个页面的xhtmlcss的不同,关键是page模板选择的问题,恒宁总结了以下两种方法。 第一种&#xff…

jquer each 遍历的结果不显示 null_SpringBoot系列(三十一)- Thymeleaf如何用th:each 做条件遍历

步骤1:基于前面的知识点步骤2:先运行,看到效果,再学习步骤3:模仿和排错步骤4:TestController步骤5:普通遍历步骤6:带状态的遍历步骤7:结合 select步骤8:结合 单选框步骤9:完整的 test.html步骤10:重启测试步骤 1 : 基于前面的知识点本知识点是建立在上一…