队列的基本概念
队列是一种线性结构,一种抽象的结构,可以用数组实现或者链表实现。
用数组实现的队列叫顺序队列,用链表实现的队列叫链式队列。
队列的特点就是先进先出,先进队列,后面的元素想出队列必须要排队。 引出两个基本的操作,入队enquence()和出队dequence()。
队列是一种很基础的结构,应用非常广泛,比如循环队列、阻塞队列、并发队列,
基于java构建顺序队列基本结构
入队:新元素放在队列的时候,只允许在队尾放进元素,新元素成为队尾。
出队:只允许把队头元素移出元素,出队元素的后一个元素成为新的队头。
队头:front
队尾:rear
下面是构建队列的代码:
package java_test;
public class MyQuence {
private String[] arr; //数组
private int n = 0; //数组大小
private int head = 0;//队头
private int tail = 0;//队尾
private MyQuence(int capacity){
arr = new String[capacity];
n = capacity;
}
public Boolean equence(String iterm ){
if (tail ==n){
return false;
}
arr[tail] = iterm;
++tail;
return true;
}
public String dequence(){
if (head ==tail ) return null;
String ret = arr[head];
++head;
return ret;
}
public void output(){
for (int i=0; i<n;++i){
System.out.print("对垒"+arr[i]);
}
System.out.println(" ");
}
public static void main(String[] args) {
MyQuence myQuence = new MyQuence(3);
myQuence.equence("chen");
myQuence.equence("asddasf");
myQuence.equence("kasda");
myQuence.output();
myQuence.dequence();
myQuence.output();
}
}
看上去好像很复杂,但其实只要指定两个指针即可。一个是head指针,指向队头,一个是tail指针,指向队尾。一开始tail和head都指向数据的坐标0位置
当元素进队的时候,tail指针往队尾移动,这样tail就会越来越大,head就自动成为了队头,这时候出队就从队头开始出,即head=0的时候,head也往后移。等到队头=队尾的时候,表示队列为空。
这样随着进队、出队的操作执行,head、tail都往后移动,当tail移动到最后的时候,即使数组有剩余空间也无法添加数据了。
如果解决这种问题呢,数据迁移,当没有空间的时候触发一次数据迁移。修改一下入队函数如下:
public Boolean equence(String iterm){
if (tail ==n){
if (head==0) return false;
for (int i= head; i<tail; ++i){
arr[i-head] = arr[i];
}
tail = tail -head;
head = 0;
}
arr[tail] = iterm;
++tail ;
return true;
}
循环队列
数据迁移操作后,入队操作性能就会受到影响,那如何避免数据迁移呢,就引出了循环队列。
队列大小为8,当head=4的时候,tail=7,当有一个新元素进入的时候,我们不把tail更新为8,而是往后移动一位,到下标为0的位置。当再有一名元素进队,把新元素放到下标为0。就变成了这样。
这样就避免了数据迁移。针对于循环队列,关键要判断队空和队满。
队空的条件还是tail=head。
队满的条件则是(tail+1)%n=head
tail =3,head = 4,n=8,(3+1)%8=4
此外,tail指向的位置实际上是没有存储数据的,所以循环队列会浪费一个数组块的存储空间
循环队列代码如下:
package java_test;
public class CircularQuene {
private String[] arr;
private int n = 0;
private int head = 0;
private int tail = 0;
public CircularQuene(int capacity){
arr = new String[capacity];
n = capacity;
}
public boolean enquence(String iterm){
if ((tail+1)%n==head) return false;
arr[tail]= iterm;
tail = (tail+1)%n;
return true;
}
public String dequence(){
if (head ==tail) return null;
String ret = arr[head];
head =(head+1)%n;
return ret;
}
public static void main(String[] args) {
CircularQuene circularQuene = new CircularQuene(5);
circularQuene.enquence("as");
circularQuene.enquence("as");
circularQuene.enquence("as");
circularQuene.enquence("as");
circularQuene.enquence("as");
circularQuene.enquence("as");
circularQuene.enquence("as");
circularQuene.enquence("as");
circularQuene.enquence("as");
circularQuene.enquence("as");
circularQuene.enquence("as");
}
}
阻塞队列
阻塞队列就是在队列的基础上加上了阻塞操作,当队列为空的时候,从队头取数就会被阻塞,因为此时没数可取,直至有数据了才返回。
当队列满了后,插入数据则会被阻塞,直至有了空闲数据再插入数据,然后返回。
阻塞队列的定义也是一个”生产者-消费者模型“,可以有效协调生产和消费的速度。当生产者生产数据块,消费者来不及消费的时候,存储数据的队列就很快被填满,生产者就阻塞。直至有了消费者消费了数据,生产者才会被唤醒进行生产数据。
并发队列
多线程的情况下操作队列,就会存在线程安全问题。
线程安全的队列称为并发队列。最简单的实现方式就是再enquence()方法、dequence()方法进行加锁,但是锁 粒度并发度会比较底,同一时刻只允许一个存一个取。实际上如果是基于数组的循环队列,可以利用CAP原子操作实现高效的并发队列。
线程池没有空闲线程,有新请求时,线程池如何处理。
1.非阻塞的处理方式,直接拒绝任务请求。
2.进行阻塞的处理方式,等到有空闲线程再取出排队中的请求。那么如何存储排队的请求呢
如果希望先来先到、比较公平的方式的话,可以用队列存储。但是队列有基于数据的顺序队列和基于链表的链表队列。
基于链表的队列可以支持无限排队的无界队列,但是过多的请求导致过多的排队,请求处理的响应时间过长。所以针对响应时间比较敏感的系统,是不合适使用基于链表无线排队的线程池的。
基于数据的队列,队列大小有限,排队的请求超过队列大小的时候,接下来的请求会被拒绝。这种方式对响应时间敏感的系统比较合理。但是如何设置一个合适 的队列大小也是很讲讲究的。
对于大部分资源有限的场景,当没有空闲资源的时候,基本可以通过队列来实现请求排队。