09.队列

09.队列:队列在线程池等有限资源池中的应用

markdown文件已上传至github

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

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

这个问题就要用带队列。

1.如何理解“队列”?

可以想象成排队买票,先来的先买,后来的1只能站在队尾,不允许插队。先进先出就是典型的队列。

队列支持两个操作:入队(放一个数据到队列尾部)、出队(从队列头部去一个元素)。

队列和栈一样,也是一种“操作受限的线性表数据结构”。

队列的应用也非常广泛,特别是一些具有某些额外特性的队列,如:循环队列、阻塞队列、并发队列。

2.顺序队列和链式队列

队列是一种抽象的数据结构。具有先进先出的特性,支持在队尾插入元素,在队头删除元素。

用数组实现的队列叫做顺序队列,用链表实现的队列叫做链式队列

2.1 基于数组的队列实现方法

队列基于数组实现的Java代码:


// 用数组实现的队列
public class ArrayQueue {
  // 数组:items,数组大小:n
  private String[] items;
  private int n = 0;
  // head表示队头下标,tail表示队尾下标
  private int head = 0;
  private int tail = 0;

  // 申请一个大小为capacity的数组
  public ArrayQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入队
  public boolean enqueue(String item) {
    // 如果tail == n 表示队列已经满了
    if (tail == n) return false;
    items[tail] = item;
    ++tail;
    return true;
  }

  // 出队
  public String dequeue() {
    // 如果head == tail 表示队列为空
    if (head == tail) return null;
    // 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了
    String ret = items[head];
    ++head;
    return ret;
  }
}

队列有两个指针,head指针指向队头,tail指针指向队尾。

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

img

调用两次出队后:

img

随着不停地出队和入队,队头和队尾指针都会往后移动,当tail移动到最右边,即使数组中还有空闲空间,也无法往队列中添加数据了。

之前数组那一章也遇到了类似的问题,数组的删除操作会导致数组中的数据不连续,我们通过数据搬移来解决这个问题。

每次出队我们都相当于删除数组下标为0的数据,然后进行数据搬移。这样的一次出队的时间复杂度就会从原来O(1)变为O(n)。能不能进行优化呢?

我们在出队的时候可以先不用搬移数据,只需要在入队的时候没有空闲空间了,再集中触发一次数据的搬移操作。所以,dequeue()保持不变,改造一下入队函数enqueue()。


   // 入队操作,将item放入队尾
  public boolean enqueue(String item) {
    // tail == n表示队列末尾没有空间了
    if (tail == n) {
      // tail ==n && head==0,表示整个队列都占满了
      if (head == 0) return false;
      // 数据搬移
      for (int i = head; i < tail; ++i) {
        items[i-head] = items[i];
      }
      // 搬移完之后重新更新head和tail
      tail -= head;
      head = 0;
    }
    
    items[tail] = item;
    ++tail;
    return true;
  }
img

这样出对操作的时间复杂度任然是O(1),入队操作的时间复杂度还是O(1)。

2.2 基于链表的队列实现方法

img

入队:

tail->next=new_node;
tail = tail->next;

出队:

head = head -> next

3.循环队列

顺序队列,当tail==n时,会有数据搬移操作,这样入队操作性能会受到影响。用数组实现的循环队列可以解决这个问题。

循环队列:把队列首尾相连,构成一个环。

img

当前head=4,tail=7,插入两个元素后循环链表如下:

img

要想写出没有BUG的循环队列的实现代码,关键是:确定好队空和队满的判定条件。

队满时:(tail+1)%n=head

img

如上图即为队满状态。tail指向的位置实际上是没有存储数据的,所以循环队列会浪费一个数据的存储空间。

如果tail直接指向最后一个元素,不浪费一个数据的存储空间,就无法区分队满和队空了,所以按以上设计。

循环队列实现代码:


public class CircularQueue {
  // 数组:items,数组大小:n
  private String[] items;
  private int n = 0;
  // head表示队头下标,tail表示队尾下标
  private int head = 0;
  private int tail = 0;

  // 申请一个大小为capacity的数组
  public CircularQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入队
  public boolean enqueue(String item) {
    // 队列满了
    if ((tail + 1) % n == head) return false;
    items[tail] = item;
    tail = (tail + 1) % n;
    return true;
  }

  // 出队
  public String dequeue() {
    // 如果head == tail 表示队列为空
    if (head == tail) return null;
    String ret = items[head];
    head = (head + 1) % n;
    return ret;
  }
}

4.阻塞队列和并发队列

平时业务开发不大可能从零实现一个队列,甚至都不会直接用到,而有一些特殊特性的队列应用却比较广泛。比如阻塞队列和并发队列。

阻塞队列:在队列的基础上加了阻塞操作。当队列为空的时候,从队头取数据会被阻塞;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

img

上述的定义就是一个生产者-消费者模型,我们可以使用阻塞队列轻松实现一个生产者-消费者模型。

这种基于阻塞队列的生产者-消费者模型,可以有效地协调生产和消费的速度。当生产者生产数据过快,消费者来不及消费,队列很快就满了,这时生产者阻塞等待,直到消费者消费了数据。

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

img

在多线程情况下,会有多个线程同时操作队列,如何实现一个线程安全队列呢?

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

5.解答开篇

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

有两种策略:

  • 非阻塞处理方式,及直接拒绝任务请求。
  • 阻塞处理方式,即将请求排队,等有空闲线程时,取出排队的请求继续处理。

那么,如何处理排队的请求呢?

为了公平处理每个排队的请求,先进着先服务,所以用队列来存储排队请求。

基于链表的队列是支持无限排队的误解队列,导致过多的请求排队等待,请求处理的响应时间过长,所以基于链表实现的无限排队线程池对响应时间较敏感的系统是不合适的。

基于数组实现的有界队列,队列大小有限,线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种的方式对响应时间敏感的系统来说就相对更加合理。设置一个合理的队列大小也是比较讲究的。

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

6.参考

这个是我学习王争老师的《数据结构与算法之美》所做的笔记,王争老师是前谷歌工程师,该课程截止到目前已有87244人付费学习,质量不用多说。

在这里插入图片描述

截取了课程部分目录,课程结合实际应用场景,从概念开始层层剖析,由浅入深进行讲解。本人之前也学过许多数据结构与算法的课程,唯独王争老师的课给我一种茅塞顿开的感觉,强烈推荐大家购买学习。课程二维码我已放置在下方,大家想买的话可以扫码购买。

在这里插入图片描述

本人做的笔记并不全面,推荐大家扫码购买课程进行学习,而且课程非常便宜,学完后必有很大提高。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/supreme_1/article/details/107679656
今日推荐