先科普一下队列:
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素成为出队。因为队列只允许在一段插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。[1]
说到队列就会想到链表,那么他们的关系是怎么样的?
链表是一种数据结构,而队列是一种抽象的概念,就像栈一样。
船是一个比较抽象的概念,具体实现有木船、铁船等等。队列好比是船,链表好比是造船的材料。
队列可以用链表实现,也可以用动态数组实现,这个抽象的概念可以用各种具体的数据结构实现。
在队列的形成过程中,可以利用线性链表的原理,来生成一个队列。基于链表的队列,要动态创建和删除节点,效率较低,但是可以动态增长。
队列采用的FIFO(first in first out),新元素(等待进入队列的元素)总是被插入到链表的尾部,而读取的时候总是从链表的头部开始读取。每次读取一个元素,释放一个元素。所谓的动态创建,动态释放。因而也不存在溢出等问题。由于链表由结构体间接而成,遍历也方便。
下面看一个队列的链表实现demo1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66class QueueNode {
Object data; // 节点存储的数据
QueueNode next; // 指向下个节点的指针
public QueueNode() {
this(null, null);
}
public QueueNode(Object data) {
this(data, null);
}
public QueueNode(Object data, QueueNode next) {
this.data = data;
this.next = next;
}
}
public class QueueLinked {
QueueNode front; // 队首指针
QueueNode rear; // 队尾指针
public QueueLinked() {
this.rear = null;
this.front = null;
} //将一个对象追加到队列的尾部
public void enqueue(Object obj) {
//如果队列是空的
if (rear == null && front == null) {
rear = new QueueNode(obj);
front = rear;
} else {
QueueNode node = new QueueNode(obj);
rear.next = node;
rear = rear.next;
}
}
//队首对象出队
//return 出队的对象,队列空时返回null
public Object dequeue() {
//如果队列空
if (front == null) {
return null;
}
//如果队列中只剩下一个对象
if (front == rear && rear != null) {
QueueNode node = front;
rear = null;
front = null;
return node.data;
}
Object obj = front.data;
front = front.next;
return obj;
}
public static void main(String[] args) {
QueueLinked q = new QueueLinked();
q.enqueue("张三");
q.enqueue("李斯");
q.enqueue("赵五");
q.enqueue("王一");
for (int i = 0; i < 4; i++) {
System.out.println(q.dequeue());
}
}
}
队列善于处理插入和弹出的操作,再简化点说,队列适合用于流水线式的任务调度,比如大家熟悉的Handle消息队列处理,EventBus中的event分发,Afinal中Http request请求发送,下面就从源码角度看看队列的应用。
Handle消息队列
Handle 异步处理中用来存放Message对象的数据结构,按照“先进先出”的原则存放消息。存放并非实际意义的保存,而是将Message对象以链表的方式串联起来的。MessageQueue对象不需要我们自己创建,而是有Looper对象对其进行管理,一个线程最多只可以拥有一个MessageQueue。在Lopper方法中:出现了一个死循环,从队列中不断的取出message,执行msg.target.dispatchMessage(msg);
1 | public static final void loop() { |
当添加Message的时候,调用MessageQueue 的enqueueMessage(msg, uptimeMillis)方法
1 | final boolean enqueueMessage(Message msg, long when) { |
这里可以看到添加一个message的时候,尾指针指向的改变。
EventBus中队列的使用
在eventbus 中队列节点的结构
1 |
|
队列的实现
1 | package de.greenrobot.event; |
event分发的逻辑
1 | final class HandlerPoster extends Handler { |
同样有个死循环,在不断的取出event,通过eventBus.invokeSubscriber(pendingPost);分发出去
看了上面的源码很容易就会发现,队列很方便的就实现的一个工作池的模型,一边在往池子里面放任务,一边在不断的从池子里面取出任务,那么在我们的实际项目中应该如何应用队列尼?
不久前,我在实现语音合成功能的时候,就发现如果用户连续的触发了两句话的发声,就会出现一起朗读的现象,那么就需要实现,不管同时触发多少个发声任务,都让他们按照先进先出的原则,先触发的先读,依次朗读,这样才是合理的,那么我就使用队列实现了一个线程池,线程池?对,说到线程池,它的内部实现也是利用队列,不信请看:ThreadPoolExecutor 的内部变量
1 | private final BlockingQueue<Runnable> workQueue; |
也是一个队列,使用的时候同样是个死循环
1 | final void tryTerminate() { |
想到了这里,我忽然发现自己好像掌握了一门异步任务处理的大杀器。
看见多任务,不怕不怕啦,队列搞死他,不怕不怕不怕啦。