注意:
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指针,指向队尾。
我们来通过一幅图来理解——
然后呢,我们调用两次出队操作——
是的,随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动(tail也会往后哦~~)。当 tail 移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。
那么这个问题该如何解决呢?
我们之前碰到过,数组的删除操作会lead to 数组中的数据data不连续。当时用了什么method呢?——数据搬移
每次出队操作都相当于删除数组下标为0的数据,if我们要搬移整个队列中的数据,这样出队操作的时间复杂度就会从O(1)变为O(n),我们怎么做可以优化这个呢?
实际上, 我们可以在出队时不用搬移数据,。
如果没有空闲空间了,我们just需要在入队时,再集中触发一次数据的搬移操作。
也就是说,我们不改变出队函数,改一下入队函数就可以。
我们来给一张示意图——
给一个思考,这样的method, 出队操作的时间复杂度仍然是O(1),那么入队操作的时间复杂度呢?分析一下。
Then, 我们来看下基于链表的队列实现方法。
同样的,我们需要2个指针。分别指向链表的第一个结点和最后一个结点。我们来看示意图——
循环队列
循环队列,它的产生是为了避免之前的一个问题。
上面我们用数组来实现队列的时候,在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 依次入队以后,循环队列的元素编程了下面这样——
通过上述的循环队列的操作,我们避免了数据搬移的操作,但是,要写好循环队列的实现代码,最关键的是——确定好队列为空和队列为满的判定条件。
我们来看看判定条件都有哪些注意的——基于数组实现的、非循环队列中,队满的判断条件是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的请求就会被拒绝。
队列可以实现的场景有哪些?
比如数据库连接池等。实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。