队列在线程池或其他有限资源池中的应用Application

注意:

CPU资源是有限的,任务的处理速度(velocity of processing)与线程个数(numbers of Thread)并不是线性正相关。On the contrary, 过多Too many的线程反而会导致lead to CPU频繁切换,处理性能下降。

Thereofore, 线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,从而来事先设置。

当我们向一个固定大小的线程池中请求一个线程时,but如果现在线程池中没有spare空闲资源了,那么now线程池会如何处理process这个request请求?拒绝还是排队请求处理策略又是什么?


上面所提出的问题和解决方案,就是我们今天要讲的主题——队列。

先进者先出就是典型的队列。
队列和栈其实是类似的,类似在哪里呢?——只支持两个基本操作——入队和出队

我们来看一张对比图——


队列和栈对比图

Queue的应用:比如高性能队列 Disruptor、Linux 环形缓存,都用到了循环并发队列;Java concurrent 并发包利用 ArrayBlockingQueue 来实现公平锁等。


顺序队列和链式队列

基于数组实现的队列: 对于栈来说,we just need 一个 栈顶指针就够了。
队列则需要两个,分别一个是head指针,指向队头;另一个是tail指针,指向队尾。


我们来通过一幅图来理解——


当 a、b、c、d 依次入队之后,队列中的 head 指针指向下标为 0 的位置,tail 指针指向下标为 4 的位置

然后呢,我们调用两次出队操作——

队列中 head 指针指向下标为 2 的位置,tail 指针仍然指向下标为 4 的位置

是的,随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动(tail也会往后哦~~)。当 tail 移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。

那么这个问题该如何解决呢?
我们之前碰到过,数组的删除操作会lead to 数组中的数据data不连续。当时用了什么method呢?——数据搬移

每次出队操作都相当于删除数组下标为0的数据,if我们要搬移整个队列中的数据,这样出队操作的时间复杂度就会从O(1)变为O(n),我们怎么做可以优化这个呢?

实际上, 我们可以在出队时不用搬移数据,。

如果没有空闲空间了,我们just需要在入队时,再集中触发一次数据的搬移操作。

也就是说,我们不改变出队函数,改一下入队函数就可以。

我们来给一张示意图——


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

给一个思考,这样的method, 出队操作的时间复杂度仍然是O(1),那么入队操作的时间复杂度呢?分析一下。


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

同样的,我们需要2个指针。分别指向链表的第一个结点和最后一个结点。我们来看示意图——

入队时,tail->next= new_node, tail = tail->next;出队时,head = head->next

循环队列

循环队列,它的产生是为了避免之前的一个问题。

上面我们用数组来实现队列的时候,在tail==n的时候,会产生数据搬移操作,In this way, 入队函数就要修改,我们来看看循环队列的解决思路——


循环队列和数组的区别:

数组是有头有尾的,一条直线。

然而循环队列则是首尾相连,扳成了一个环。我们来看张图——

循环队列示意图

我们可以看到,这个队列的大小为8,当前head为4,tail为7。when有一个新的元素a入队的时候,我们把a放入下标为7的位置。但这个时候,我们并不需要把tail更新为8,**而是将tail在整个环中后移一位,到下标为0的位置。 当再有一个新的元素b入队的时候,新的元素b放入下标为0的位置 ,然后tail加1更新为1 **,因此,当新的元素a b 依次入队以后,循环队列的元素编程了下面这样——

插入元素a b后

通过上述的循环队列的操作,我们避免了数据搬移的操作,但是,要写好循环队列的实现代码,最关键的是——确定好队列为空和队列为满的判定条件

我们来看看判定条件都有哪些注意的——基于数组实现的、非循环队列中,队满的判断条件是tail==n,**队空的判断条件是head==tail。 **

那么针对于循环队列呢?——

队列为空的判断条件仍然是head == tail, 但是队列为满的条件就比较复杂了,我们来看一张队列为满的图——

循环队列队列为满的示意图

上图中,实时情况是——tail = 3, head = 4, n = 8。我们总结一下规律就是**(3+1)%8=4 **。

当我们多花了几张队满的图时,就会发现规律是——**(tail+1)%n=head **。

而且,**当队列为满时,图中的tail指向的位置实际上没有存储数据 ,也就是说,循环队列会浪费一个数组的存储空间 **。


阻发队列和并发队列

这两个队列属于比较特殊的队列。

阻发队列 , 就是在队列的基础上增加了阻塞操作。In other words, 就是在队列为空的时候,从队头取数据会被阻塞 , why? 因为此时还没有数据可以取,需要等到队列中有了数据后才能返回。

如果**队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲spare位置后再插入数据,然后再返回。 **

生产者-消费者模型

在上图的“生产者-消费者模型”中,当生产者生产的速度过快,消费者来不及消费时,存储数据的队列很快就会变满。

In this time, 生产者就会阻塞等待, 直到消费者消费了数据,生产者才会被wake up从而继续生产

of course, 我们还可以多配置2个消费者,来应对一个生产者。见图——

多个消费者

在多线程的情况下,会有多个线程同时操作队列,这个时候线程的安全性就需要我们来考虑,那么如何实现一个线程安全的队列呢?

我们来看看并发队列, 最简单的实现方式则是直接在入队和出队的函数上加锁,但是锁粒度大并发度就会比较低。同一时刻仅仅允许一个或者操作。

实际上,基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。

我们现在来考虑一个问题——

当线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?

第一种:非阻塞的处理方式,直接拒绝任务请求。

第二种:阻塞的处理方式,将请求排队,当有空闲线程时,取出排队的请求继续处理。


我们希望可以公平的处理每个排队的请求,先进者先服务。

  • 基于链表的实现, 可以实现一个支持无限排队的无界队列,但may导致过多的请求排队等待,响应时间过长。
  • 基于数组的实现, 数组实现的通常是有界队列,队列的大小有限。当请求过多时,following的请求就会被拒绝。

队列可以实现的场景有哪些?

比如数据库连接池等。实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。

阻塞队列就是入队和出队时,操作可以进行阻塞。

并发队列就是队列的操作多线程安全。

你可能感兴趣的:(队列在线程池或其他有限资源池中的应用Application)