队列
先进先出,后来的只能插入到队尾
操作
入队:enqueue(),出队dequeue()
应用
循环队列(高性能队列DIsruptor,Linux环形缓存)、阻塞队列、并发队列,Java concurrent 并发包利用 ArrayBlockingQueue 来实现公平锁等
实现
数组实现顺序队列,链表实现链式队列
队列需要两个指针,一个是head指针指向第一个数据,一个是tail指针指向下一次要插入数据的位置
数据搬移
解决数组空间用完问题。优化搬移操作:如果每删除一个数据就搬移一次,出队的时间复杂度就会由O(1)变为O(n),优化之后将出队保持不变,在入队时判断集中触发搬移操作,入队的时间复杂度还是O(1)
public boolean enqueue(String item) {
if (tail == n) {
if (head == 0) return false;
for (int i= head; i < tail ; i++ ) {
items[i - head] = items[i];
}
tail = tail - head;
head = 0;
}
items[tail] = item;
tail++;
return true;
}
循环队列
解决数据入队搬移操作影响性能问题。最主要就是判断队空和队满条件
//判断队空
head == tail
//判断队满
head == (tail + 1) % n
public boolean enqueue(String item) {
if ((tail + 1) % n == head) return false;
items[tail] = item;
tail = (tail + 1) % n;
return true;
}
public String dequeue(String item) {
if (head == tail) return null;
String ret = items[head];
head = (head + 1) % n;
return ret;
}
阻塞队列和并发队列
阻塞队列:在原来队列中添加阻塞操作,实现“生产者-消费者模型”。
并发队列:最简单直接的实现方式是直接在 enqueue()、dequeue() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用CAS 原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。
补充
当线程池没有空闲资源,这时有新的任务请求线程资源,一般有两种处理方式,一种是非阻塞处理方式,直接拒绝请求任务,一种是阻塞式处理方法,将请求排队,当有空闲资源时在将请求取出处理,这是就用到队列了。
基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue),但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的。
而基于数组实现的有界队列(bounded queue),队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说,就相对更加合理。不过,设置一个合理的队列大小,也是非常有讲究的。队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。
除了前面讲到队列应用在线程池请求排队的场景之外,队列可以应用在任何有限资源池中,用于排队请求,比如数据库连接池等。实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。