我们知道,CPU资源时有限的,任务的处理速度与线程个数并不是线性相关。相反,过多的线程反而会导致CPU频繁切换,处理能力下降。所以线程池的大小一般都是综合考虑要在处理任务的特点和硬件环境,来事先设置的。
当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?时拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?
实际上,这些问题并不复杂,其底层的数据结构就是我们今天要学的内容,队列(queue)。
如何理解队列?
队列这个概念非常好理解。你可以把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。先进者先出,这就是典型的“队列”。
队列和栈非常相似,支持的操作也很有限,最基本的操作也是两个:入队和出队
所以,队列和栈一样,也是一种操作受限的线性表数据结构。
队列的概念很好理解,基本操作也很容易掌握。作为一种非常基础的数据结构,队列的应用也非常广泛,特别是一些具有某些额外特性的队列,比如循环队列,阻塞队列,并发队列。它们在很多偏底层系统,框架,中间件的开发中,起着关键性的作用。比如高性能队列Disruptor,Linux环形缓存都用到了循环并发队列;Java concurrent并发包利用ArrayBlockingQueue来实现公平锁等。
顺序队列和链式队列
下面时简单的基于数组的顺序队列实现:
// 用数组实现的队列
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;
}
}
基于链表的队列:
循环队列:
我们刚才用数组实现队列的时候,在tail==n时,会有数据搬移操作,这样入队操作性能就会受到影响。那有没有办法能够避免数据搬移呢?我们来看看循环队列的解决思路。
循环队列,顾名思义,它长得像一个环。原本数组时有头有尾的,时一条直线。现在我们把首尾相连,形成了一个环。如图所示:
循环队列的关键是确定好队空和队满的判定条件
就像如图所示的队满的情况:(tail + 1)%n=head
但是,循环队列会浪费一个数组的存储空间。
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;
}
}
阻塞队列和并发队列
并发队列就是线程安全的队列。最简单直接的实现方式是直接在enqueue(),dequeue()方法上加上锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取。实际上,基于数组的循环队列,利用CAS原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。