前言
在netty的NioEventLoop中用到的jcTools下的MPSC队列很有必要学习下。由于MPSC队列是多生产单消费者的,MPMC是多生产者多消费者的,更有利于学习,故此篇以MpmcArrayQueue为基础。在学习之前,先简单回顾一遍队列及相关知识。
队列
队列是一种FIFO的数据结构,使用队列存取数据元素时,数据元素只能从表的一端进入队列,另一端出队列。常见于线程池的等待排队,锁的等待排队。
队列有两种实现方式(堆栈实现不讨论)
- 顺序存储实现–>数组
- 链式存储实现–>链表
Queue定义如下:
public interface Queue<E> {
void enqueue(E e);
E dequeue();
int size();
}
定义了三个方法enqueue入队,dequeue出队,size队列大小。
基于数组实现的queue:
public class ArrayQueue1<E> implements Queue<E> {
private Object[] elements;
private int size;
private int index;
private int capacity;
public ArrayQueue1(int capacity) {
this.capacity = capacity;
elements = new Object[capacity];
}
@Override
public void enqueue(E e) {
elements[index++] = e;
size++;
}
@Override
public E dequeue() {
E e = (E) elements[0];
moveLeftOneStep();
index--;
size--;
return e;
}
//左移一步
private void moveLeftOneStep() {
for (int i = 1; i < elements.length - 1; i++) {
elements[i] = elements[i + 1];
}
}
@Override
public int size() {
return this.size;
}
}
ArrayQueue1中队列通过给定一个容量进行初始化,Object数组用于存元素,index表示元素放到何处了,size表示元素的个数。入队的时候index处放元素,然后index自增1,size加1。出队的时候从头(0号位置)取,取完之后剩余的元素集体左移一步。
注:上述代码省略了边界条件校验等其它必要的校验。
ArrayQueue1的代码存在一个不好的地方:每取一次元素都要移动一次,有N个元素就要移动N-1次。如果用时间复杂度表示就是O(n),这显然是不可接受的。于是,有了第二种的数组实现队列:循环数组队列。
基于循环数组的队列
循环数组是一个如同环的数组,它用两个指针putIndex和takeIndex,移动takeIndex就可以指示出可以取的元素在哪儿,就可以不用再移动元素,如下图。
这样做有两个问题需要考虑:
- Q1:如何判断队列满或空?
- Q2:数组是线性的,有限的,指针如何进行循环?
Q1的答案有3种。
- A1:预留长度法
- A2:预留一位法
- A3:设标志位法
Q2的答案有2种。
- A1:取余
- A2:指针到达尾部置0
Q1的解决方法采用A1-预留长度法,入队+1,出队-1,size == capacity为满,size==0为空。Q2的解决方法采用A2来处理。
代码如下:
public class ArrayQueue2<E> implements Queue<E> {
private Object[] elements;
private int size;
private int putIndex;
private int takeIndex;
public ArrayQueue2(int capacity) {
this.elements = new Object[capacity];
}
@Override
public void enqueue(