Queue接口继承了Collection接口,其内部定义了六个方法,分为三大类,分别是新增元素、删除元素、检索元素。每一个大类都提供了两个方法,这两个方法的作用可以用下面的表格来描述:
(1).add()和offer():这两个方法是向队列添加元素,不同之处在于如果队列已经添加满了还继续添加add()方法就会抛出异常,而offer()会返回false值(如果队列没有满,则offer()添加成功后会返回true)。
(2).remove()和poll():这两个方法都是返回队列顶元素并且从队列中删除。不同的是,如果在删除时队列里没有元素了那么remove则会抛异常,而poll则会返回null。
(3).element()和peek():这两个方法都是取队列顶部元素但是不删除,和remove()、poll()有所区别。当队列没有元素时element()会抛异常,而peek()会返回null。
一、双向队列Deque
双向队列可以双向操作,比如添加元素可以从两头添加、删除和获取都可以从收尾删除获取,这些操作都对应于相应的last和first方法。Deque也是一个双向队列接口,其下的实现由ArrayDeque(它的大小没有限制,也可以手动限制)。
下面我们看双向队列的实现原理。我们以ArrayDeque类分析。
其实如果在创建ArrayDeque对象时不指定其初始大小它默认的就是16,如果在使用的时候发现不够就会拓展。双向队列的数据存储实际上时用数组来存储的,在ArrayDeque中有一个全局的E[] elements,数组,之后所有的增删改查都与此数据息息相关,此外还有两个比较重要的变量就是head和tail变量,分别表示队列首部和尾部。当我们从队列的头部取值时head值会自动+1,且会将elements[head]元素置为null(如果是remove方法或poll方法),如果是从队列的尾部取值,则tail的值会自动-1.其实现原理如下:
public E pollFirst() {
int h = head;
E result = elements[h]; // Element is null if deque empty
if (result == null)
return null;
elements[h] = null; // Must null out slot
head = (h + 1) & (elements.length - 1);
return result;
}
public E pollLast() {
int t = (tail - 1) & (elements.length - 1);
E result = elements[t];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}
虽然上面给出的只是pollFirst()和poolLast()方法,但是实际上poll()也是调用的pollFIrst()方法。二、阻塞队列BlockingQueue
先来总结下BlockingQueue的特性:
1、阻塞队列在我们处理生产者-消费者模式的时候是很有用的,尤其是如果有大量网络请求的应用场景。当队列里的元素为空时,他就等处于等待状态,等待队列里存储进新的任务或元素。
2、BlockingQueue的四种类型的方法,每一种类型都有不一样的操作,有时候可以根据不同的场景去使用。这四种不同的类型分别是:抛出异常、返回一个特定的值(null、false、0等)、无限期的阻塞当前线程直到操作成功、在放弃任务之前等待的时间。
3、BlockingQueue不接受null元素。
4、BlockingQueue必须有一个容量限制。
5、BlockingQueue是线程安全的,其所有的方法都有内部的Lock保证安全。
6、BlockingQueue没有任何类似“close”、“shutdown”等操作去指示不能添加更多的元素。
如下便是四种不同类型的方法,和普通的Queue就多了个阻塞机制:
Throws exception | Special value | Blocks | Times out | |
Insert | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
Remove | remove() |
poll() |
take() |
poll(time, unit) |
Examine | element() |
peek() |
not applicable | not applicable |
方法的用于上述图标已经很明了,下面我们看一个典型的生产者和消费者的例子:
(1)生产者
/**
* 生产者
*
* @author Administrator
*
*/
public static class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while (true) {
// 生产对象/任务,
queue.put(produce());
}
} catch (InterruptedException ex) {
}
}
Object produce() {
cout++;
return "第" + cout + "个任务";
}
}
(2)消费者
/**
* 消费者
*
* @author Administrator
*
*/
public static class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while (true) {
// 从队列取出元素/任务,如果没有任务就会一直处于等待状态
consume(queue.take());
}
} catch (InterruptedException ex) {
}
}
// 实际的消费行为
void consume(Object x) {
System.out.println(x);
}
}
最后开启任务:
public static int cout = 1;
public static void main(String[] args) {
BlockingQueue queue = new ArrayBlockingQueue(10);
Producer pro = new Producer(queue);
Consumer con = new Consumer(queue);
// 开始生产
new Thread(pro).start();
// 开始消费
new Thread(con).start();
}
实际上,在很多场景都有这种生产者-消费者模式,比如在Volley中其就使用到了BlockingQueue来接收请求,然后获取任务从而发起请求。(注:生产者-消费者模式很经典,在操作系统中PV锁控制的中断也可以实现类似生产者-消费者模式)。
常用的阻塞队列
其实从上面的代码可能已经猜出来了,BlockingQueue只不过是一个接口,其下有众多的不同实现类,下面就简单介绍下其实现类以及使用的场景:
ArrayBlockingQueue
一个由数组支持的有限阻塞队列(其大小在创建的时候就被确定,并且不能在修改)。此队列里存储的元素顺序是FIFO(first-in-first-out),是一个很标准的普通队列,也是我们最常使用到的阻塞队列。其头部的元素是在队列带的时间最长,尾部元素在队列中呆的时间最短,新来的元素是插在尾部的,而当队列获取元素时是从头部获取的。如果试图将一个元素put到一个full状态的队列,这个操作就会被阻塞,直到队列有空位置。如果从一个empty队列获取新的元素同样也会被阻塞,知道有元素可获取。
此类为等待的消费者/生产者提供了一个可选的公平的处理策略。此类默认的不保证阻塞的顺序。可以看到ArrayBlockingQueue有三个构造器,第一个就是只提供一个容量,第二个构造器的第一个参数就是容量,第二个参数就是是否公平加入/移除队列,如果设置为true就会按照FIFO的顺序来入队列和出队列,如果为false就不保证顺序了。一个参数的构造器默认的是false。
DelayQueue
此队列是一个无界限的队列,只有当他的元素在队列中超过规定时间了他才可以被取出来,其头部是超期时间最长的元素,尾部就是超期最短(或还未过期)的元素。如果还没有过期的元素,那么就不存在头部了,将直接返当回null,他的队列调用了元素的getDelay()方法返回的值小于或等于0就表示过期了。
这个队列不是所有的元素都可以放进去的,必须是实现了Delayed接口的类对象才可以放进去,否则就会报类型转换异常,那么什么是Delayed接口呢?它很简单,直接去看其API吧!我们暂时只需要知道如何使用就行!下面给出一个场景用DelayQueue实现:
作为上班族是不能迟到早退的,不然扣钱扣得那叫一个惨不忍睹....亲身体验,我有一次就年会那天直接去年会现场了,没有来打卡就扣了好几百啊有木有,就半天时间啊有木有,说多了都是泪...言归正传。有一天Boss让小A去加个班,并让小A不管工作有没有做完必须在晚上24点才能下班(我们先假设这个工作加班到死也一下子做不完),在小班后小A还得打卡验证。那么程序如何模拟呢?下面上代码:
(1)老板类
/**
* 老板
*
* @author Administrator
*
*/
public static class Boss implements Runnable {
private final BlockingQueue queue;
Boss(BlockingQueue q) {
queue = q;
}
public void run() {
try {
// 告诉小A去加班啊,别偷懒,下班记得在24点打卡啊
queue.put(produce());
// }
} catch (InterruptedException ex) {
}
}
private Object produce() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
// 这里指定啥时候下班:老板让他在24点下班
Date date = sdf.parse("2089-06-18 24:00:00");
// 构造器传递一个long类型的数值
return new DelayObject(date.getTime());
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
(2)小A
/**
* 存放到DelayQueue的元素必须实现Delay接口
*
* @author Administrator
*
*/
private static class 小A implements Delayed {
static int count = 1;
static long delay = 1;
public 小A(long delay) {
this.delay = delay;
count++;
}
public int compareTo(Delayed o) {
return 0;
}
/**
* 队列会不停调用此方法(比如take()方法里有个死循环,会不断调用getDelay())判断该元素是否可以取出来,
* 因此此参数不应该是一个固定的,应该和系统时间关联,即随着时间的推移 可以使得这个元素可以使用
* 当这个返回值小于等于0的时候就可以从队列中取出这个元素了
*/
public long getDelay(TimeUnit unit) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 每次调用getDelay()时都重新计时
Date date = new Date();
String sd = sdf.format(date);
try {
Date d2 = sdf.parse(sd); // 前的时间
long diff = delay - d2.getTime(); // 两时间差,精确到毫秒
// 此diff会1秒秒的减小,当其为0的时候就不再调用了,直接就可以出队列了。
return diff;
} catch (ParseException e) {
e.printStackTrace();
}
return 0;
}
/**
* 下班了
*/
public void free() {
System.out.println("哈哈哈哈哈哈---我下班了");
}
}
(3)考勤系统
/**
* 公司的考勤系统
*
* @author Administrator
*
*/
public static class SystemWorker implements Runnable {
private final BlockingQueue queue;
SystemWorker(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while (true) {
// 只要还没到下班时间,他就一直处于阻塞状态,等待
offWork(queue.take());
}
} catch (InterruptedException ex) {
}
}
// 下班打卡:其参数就是具体的某个员工下班
void offWork(Object x) {
小A delay = (小A) x;
// 如果能下班就说
delay.free();
}
}
(4)正式运作
BlockingQueue queue = new DelayQueue();
Boss pro = new Boss(queue);
SystemWorker con = new SystemWorker(queue);
// 老板发话
new Thread(pro).start();
// 系统开始工作
new Thread(con).start();
以上就是三个角色完成的上述场景,注意啊,小A这个类的命名我用了个中文哈(逼死强迫症 T_T )。好,如上就是DelayQueue的工作场景和使用方法了,这里总结下,首先主要就是确定未来时间点,也就是任务结束的时间,并且在getDelay()方法中动态的获取当前时间进行计算,不断地缩小差值,也就是说DelayQueue需要我们手动计算延时操作,这样我们可以在遇到突发状况时可以随时更改此时间差值以提前完成任务。LinkedBlockingQueue
是一个基于连接节点的任意大小容量的队列,这个队列的顺序是FIFO,其头部是存入队列最早的元素,尾部是存入队列最晚的元素,每次都是讲元素插入尾部,从头部取出元素。LinkedBlockingQueue:比ArrayBlockingQueue有更大的吞吐量,但是在并发的情况下其性能是不可预测的。
好吧!暂时就介绍以上三种常见常用的队列,另外还有LinkedTransferQueue、PriorityBlockingQueue、SynchronousQueue三种大家可以自行查看API使用。