07| 队列:如何在线程池等有限资源中应用?


大家好,我是热爱编程的斌斌。

大家都知道,CPU是有处理上限的,也就意味着单位时间内只能处理有限数量的线程,线程又是从线程池中获得的,也就是说线程池的大小也是有限的。那么,如果此时线程池的资源已经被用完了,那新来的请求如何处理呢?是拒绝还是排队请求?各种处理出了事如何实现的那?

那就让我们带着这个问题往下看下去,相信你会找到答案!

一、如何理解队列?

和栈一样,队列也只有两个操作:入队列和出队列,是一种操作受限的数据结构。入队列:将一个元素插入到队列的末尾;出队列:从对头获取一个元素。如下图所示,简单理解,就好比排队买票,排在前面的人优先买到票,后面的人只能等前面的人处理完毕之后才能处理。也就是:先进先出
在这里插入图片描述
上面对队列的概念和操作进行了简单的描述,也很容易理解。不仅如此,队列的应用也特别广泛,下面就来看看五个特殊的队列。

二、顺序队列与链式队列

1.如何实现一个队列?

你发现没有?在数据结构的学习中,我们首先接受接触了简单的类型:数组和链表,后面学习的内容复杂度成上升趋势。这还不是主要的规律,主要的是:后面的复杂数据结构几乎可以给予前面的数组和链表来实现。这里的队列也不例外,它有两种实现,一种是顺序队列;另一种是链式队列。

方便理解,废话少说,上代码:
顺序队列

public class ArrayQueue {
    private String[] items;
    private int n = 0;

    //head指向队头,tail指向队尾,tail指向空
    private int head = 0;
    private int tail = 0;

    //初始化队列,根据长度
    public ArrayQueue(int capacity) {
        items = new String[capacity];
        n = capacity;
    }

    //入队
    public boolean enQueue(String item) {
        //队列满了
        //队列的元素一直入队出队,head和tail指针会一直往后移,直至队列末尾没有空间,就填入不了元素了
        //不用每次出队列都搬移元素
        //而是,在当存满了之后,再入队列,集中执行一次搬移操作
        if (tail == n) {
            //如果队列是满的,就不存
            if (head == 0) return false;

            //队列没有存满,但tail已经指向最后一个元素
            //进行数据搬移:将[head,tail-1]的所有元素搬到0开始长度为tail-head
            for (int i = head; i < tail; i++) {
                items[i - head] = items[i];
            }
            tail = tail - head;
            head = 0;
        }
        //入队列

        items[tail++] = item;
        return true;
    }

    //出队
    public String deQueue() {
        //队列为空
        if (head == tail) return null;
        return items[head++];
    }

}

上面是对队列优化之后的代码,那对哪一点进行了优化呢?

首先来说说队列的实现,设置了两个指针,head指向对头,tail指向队尾。队列在不断的入队列和出队列的过程中,head和tail两个指针会不停地往后边移动,当tail移动到数组最后一个位置的时候,已经没有插入的空间了。这里,分为两种情况,一种是队满,一种是队列未满。如果队列已经满了,可以拒绝插入,返回false;如果数据未满,那就需要搬移元素。
在这里插入图片描述

那在哪个操作进行数据搬移呢?入队列还是出队列?入队列比较简单,所以应该不要给太多实现,那就把数据搬移放到出队列。当tail指向队尾了,在出队列时,不要进行任何操作;在入队列时,集中执行一次数据搬移操作。
在这里插入图片描述

链式队列
链式队列和顺序队列思路一直,只不过把数组变成了链表,如图所示:
在这里插入图片描述

代码实现:

2.普通队列中存在什么问题?

上面说过,队列在tail指向队尾的时候,再插入数据时需要搬移数据,数组中搬移数据的时间复杂度为O(n),耗时比较长,如何来解决这个问题呢?下面讲到的循环队列就是来解决这个问题的!

三、循环队列

1.如何理解循环队列?

看下面这幅图,它是不是长得很像一个环?这就是一个循环队列,和普通的队列一样,它也有两个指针,一个指向对头,一个指向队尾。队列是怎么解决数据搬移的呢?咱们分析一下下面这种情况,当前队列的小为8,当tail指向7,也就是队列最后一个元素。继续往队列中插入a和b元素。当a插入数组时,并不需要搬移元素,而是把tail指针在环中往前面移动一个位置,指向0.
在这里插入图片描述
接着把b插入到0的位置,在插入a和b之后循环队列变成了下面这个样子:

在这里插入图片描述

2.如何实现循环队列?

要实现一个循环队列,就离不开队列的两个操作:出队列和入队列。只有数组还有空间时,才能入队列;只有队列不为空才能出队列。这就决定了循环队列实现的两个核心点:队空和队满的判断条件。

在没有元素插入时,队列处于队空状态,此时tail和head两个指针都指向第一个元素,也就是headtail时,队列处于队空;经过简单的列举法,你会发现,当满足(tail+1) % nhead时,队列处于队满的状态。

3.代码实现:


public class CircularQueue {
    private String[] items;
    private int n = 0;

    private int head = 0;
    private int tail = 0;

    public CircularQueue(int capacity) {
        items = new String[capacity];
        n = capacity;
    }

    public boolean enQueue(String item) {

        //表示入队列的位置
        int loc = (tail + 1) % n;
        //队列满了
        //tail指向了最后一个位置
        //tail + 1 表示在顺序队列中的位置
        //%n表示相对再循环队列中的位置
        if (loc == head) return false;

        items[loc] = item;
        tail = loc;

        return true;
    }

    public String deQueue() {
        //队列为空
        if (head == tail) return null;

        String ret = items[head];
        head = (head + 1) % n;
        return ret;
    }
}

四、阻塞队列和并发队列

1.阻塞队列

首先,队列在什么时候可以处于阻塞状态?在队满情况,还有入队列操作时,队列就可以处于阻塞状态,直到数组中有空闲空间,阻塞状态结束;同样,在队空的情况下,好存在出队列的时候,队列可以处于阻塞状态,直到有元素插入队列时,阻塞状态结束。很好理解,添加了阻塞操作的队列就是阻塞队列。

在这里插入图片描述
阻塞队列的特点非常像“生产者-消费者”模式,使用阻塞队列可以实现这种模式,进而有效协调生成者和消费者的速度。当“生产者”生产过快,“消费者”来不及消费时,可以让生产者进入阻塞状态,直到消费者消费了之后,才唤醒“生产者”继续生成。除此之外,还可以协调“生产者”和“消费者”的个数,来提高数据的出列效率。对应上面的情况,我们可以设置多个“消费者”对应一个“生产者”。
在这里插入图片描述

2.并发队列

线程安全的队列被称为并发队列。如何实现线程安全呢?最直接的方式:把入队列和出队列方法填上锁。但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用CAS原子操作,可以实现非常高效的并发队列。

四、解答开篇

五、总结

队列最大的特点就是先进先出,主要的两个操作是入队和出队。跟栈一样,它既可以用数组来实现,也可以用链表来实现。用数组实现的叫顺序队列,用链表实现的叫链式队列。特别是长得像一个环的循环队列。在数组实现队列的时候,会有数据搬移操作,要想解决数据搬移的问题,我们就需要像环一样的循环队列。循环队列是我们这节的重点。要想写出没有bug的循环队列实现代码,关键要确定好队空和队满的判定条件,具体的代码你要能写出来。除此之外,我们还讲了几种高级的队列结构,阻塞队列、并发队列,底层都还是队列这种数据结构,只不过在之上附加了很多其他功能。阻塞队列就是入队、出
队操作可以阻塞,并发队列就是队列的操作多线程安全。

六、课后思考

  1. 除了线程池这种池结构会用到队列排队请求,你还知道有哪些类似的池结构或者场景中会用到队列的排队请求呢?
  2. 今天讲到并发队列,关于如何实现无锁并发队列,网上有非常多的讨论。对这个问题,你怎么看呢?
    1.分布式应用中的消息队列,也是一种队列结构
    2.考虑使用CAS实现无锁队列,则在入队前,获取tail位置,入队时比较tail是否发生变化,如果否,则允许入队,反之,本次入队失败。出队则是获取head位
    置,进行cas。
  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值