- 注:本篇内容参考了《Java常用算法手册》、《大话数据结构》和《Java编程(第四版)》三本书籍,并参考了在线工具网站中对于队列相关Java类的一些说明。
- 文中涉及的java以Java 1.8为准。
本人水平有限,文中如有错误或其它不妥之处,欢迎大家指正!
目录
1. 队列
1.1 队列定义
队列(Queue)结构是从数据的运算来分类的,换句话说,就是队列具有特殊的运算规则。从数组的逻辑结构来看,队列其实是一种线性结构。队列的元素类型是相同的。
队列是允许对两端进行操作,在表的一端只能进行删除操作,此端称为队头;在表的另一端只能进行插入操作,称为队尾。若队列中没有数据,则称为空队列。队列是按先进先进(First In First Out,FIFO)的原则处理结点数据的,即插入的顺序和取出的顺序是相同的。它常被当作一种可靠的将对象从程序的某一个区域传输到另一个区域的途径,比如常用的消息队列,还有并发编程中也会用到。队列好比日常生活中的排队,比如去买票,先排的在前面,就会先买到票,类似先来先服务的原则。
在硬件的存储芯片中,有一类根据队列结构构造的芯片,那就是FIFO芯片。这类芯片具有一定的容量,保留了一端作为数据的存入,另一端作为数据的读出。先存入的数据将先被读出。
1.2 队列的特点
从队列的定义中可以看出,队列具有以下特点:
- 是一种线性结构,若队列中没有数据,称为空队列;
- 允许对两端进行操作,一端只能删除,此端称为队头;另一端只能插入,称入队尾,即删除和插入在不同的两端进行;
- 按照先进先出(First In First Out,FIFO)原则处理数据;
1.3 队列的分类
从逻辑结构上,一般将队列分为队列、循环队列和双向队列。其中队列在上面已经描述过了。
循环队列是头尾相接的顺序存储结构的形式,下方会有详细描述。
双向队列,也叫双端队列,它是允许可以在队列的任何一端添加或移除元素的队列。下方会有详细描述。
1.4. 队列的操作
在队列结构中,数据运算比较简单。一般队列的基本操作包含入队列和出队列两个。除此之外,还需要有初始化队列、获取队列长度等操作。接下来看下队列的具有实现。
1.4.1 入队列
将一个元素添加到队尾,相当于到队列的最后排除等候。队列中前面的元素不变,即入队列不影响队列中的其它元素。
实际生活中的排队,也是这样的,新来的排在队列的最后面。
1.4.2 出队列
将队头的元素取出,同时删除该元素,使后一个元素成为队头。队列中的元素会全部向前移动一个位置。以保证队头不为空,此时时间复杂度为O(n)。
比如在排队买票时,第一个人买完票走了,那他后面的一个人就是第一个要买票的人了,就是新的队头。那么排队的每个人都会相应向前移动一个位置。
为什么这里不说从队列的中间出队列呢?因为队列的出队列只允许在队头操作。
1.5 队列的应用
在实际生活中,队列的例子随处可见。比如以前需要排队买火车票,医院的挂号等。队列的核心是先进先出,所以一般涉及到先进先出的问题,可能很多时候都用采用队列来处理。
在数据结构和算法领域,可以使用队列来解决迷宫问题,也可以解决图的广度优先遍历。
在普通开发中,可以确定长度最大值的情况下,建议使用循环队列;若无法预估队列的长度时,则用链队列。
消息中间件中也有队列的概念,先进入队列的消息一般需要先消费,用的正是队列的先进先出。在分布式中,队列可以用来解决并发、分布式事务、定时任务等问题。
2. 队列的存储结构
从数据的存储结构来划分,队列结构包含顺序存储和链式存储两种。
2.1 顺序队列结构
2.1.1 顺序队列结构概述
顺序队列结构,是使用一组地址连续的内存单元依次保存队列中的数据。可以定义一个指定大小的结构数组来作为队列。当向队列中添加一个元素时,是添加到队尾,不需要移动元素,时间复杂度为O(1)。就像排除买票,直接排放队的最后面,前面的人啥都不用干。如下图所示。
队列元素的出列是在队头,即下标为0的位置,这就意味着在队头删除元素时,队列中的所有元素都得向前移动,以保证队列的队头不为空,此时时间复杂度为O(n)。就像买票排序一样,最前面的人买完走了,后面的人都要向前移动一个位置。如下图所示。
2.1.2 出队列时全部移位
为什么在出队列时一定要全部移位呢?如果没有队列的元素必须存储在数组的前n个单元这一限制条件,出队的性能就会有所提高。也就是队头不需要一定是下标为0的位置。如下图。
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针,front指针批向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此队列是空队列了。
假如有一个长度为5的数组,初始空队列,如下图的左图所示。front和rear指针都指向下标为0的位置。然后入队a1、a2、a3、a4,front指针还是指向下标为0的位置,而rear指针指向下标为4的位置,如下图的右图所示。
接下来出队a1、a2,此时front指针指向了下标为2的位置,而rear指向不变,如下图的左图所示。再入队a5,此时front指针不变,而rear指针移动到数组之外。数组之外?就是数组越界了。如下图右图所示。
2.1.3 假溢出
问题还不止于此。假设此队列的总个数不超过5个,但目前若接着入队的话,因数组的末尾元素已经占用,再向后加就会产生数组越界的错误。然而队列的下标为0和1的地方还是空闲的。把这种现象叫做“假溢出”。即在顺序队列中,当队尾指针已经达到数组的上界时,不能再有入队操作,但其实数组中还有空位置。真溢出就是队列空间能够容纳多少,实际入队就是多少。
解决假溢出的办法就是后面满了,就再头开始,也就是头尾相接的循环(即当),也就是循环队列。把上图的rear的指向改为指向下标为0的位置,这样就不会造成指针不明的问题了。如下图。
再接着入队a6,将它放在下标为0的位置,rear指针指向下标为1的位置,如下图的左图所示。若再入队a7,则rear指针就与front指针重合了,也就是同时指向了下标为2的位置。如下图的右图所示。
2.1.4 队列空或满
此时又有问题了,上面描述了,空队列时front等于rear,现在当队列满时也是front等于near,那如何判断此时的队列究竟是空还是满呢?
第一种办法是设置一个标志变量flag,当front == rear时,且flag == 0时队列空;当front == rear时,且flag == 1时队列满。第二种办法就是当队列空时,条件就是front == rear,当队列满时修改其条件,保留一个元素空间。也就是说队列满了,数组中还一个空闲单元。如下图所示,此时认为队列满了,也就是说不允许上图中的右图情况出现。
下面重点来讨论第二种方法。由于rear可能比front大,也可能比front小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差整整一圈。所在若队列的最大大小为size,那队列满的条件是(rear + 1) % size == front,这里取模的目的就是为了整合rear与front大小为一个的问题。例如上面的例子中,size = 5,开始入队四个元素时,front = 0,而rear = 4,(4+1)% 5 = 0,此时队列满。在假溢出时使用循环队列,front = 2,rear = 0,(0 + 1) % 2 = 1,1与2不相等,所以此时队列并没有满。可以自己继续验证下其它情况。
另外,当rear > front时,如下图所示的情况。此时队列的长度为rear - front。
但当rear < front时,如下图所示的情况。此时队列的长度分为两段,一段是size - front,另一段是 0 + rear,加在一起,队列的长度为rear - front + size。因此通用的计算队列长度公式为:
(rear - front + size) % size
对于队列来说,为了避免数组插入和删除时需要移动数据,于是引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是O(n)的时间复杂度变成了O(1)。
2.2 链式队列结构
链式队列结构,使用链表形式保存队列中各元素的值。其实就是线性表的单链表,只不过它只是能尾进头出而已,简称为链队列。为了操作上的方便,将队头指针指向链队列的头结点,而队尾指针指向终端结点。如下图。
空队列时,front和rear都指向头结点。如下图所示。
链队列的入队操作如下图所示。
链队列的出队操作如下图所示。
2.3 循环队列与链队列的比较
循环队列与链队列的比较,从时间和空间两个方面来进行。
从时间上,它们的基本操作都是常数时间,时间复杂度都为O(1),不过循环队列是事先申请好空间,使用期间不释放;而对链队列,每次申请和释放结点也会存在一些时间开销,若入队出队比较频繁,那么两者差异很细微。
从空间上,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。
3. 队列的Java实现
下面是以数组实现的队列,代码和测试代码如下。
public class ArrayQueue<T> {
/**
* 队列数量
*/
private int size;
/**
* 队列的默认数量
*/
private static final int DEFAULT_SIZE = 64;
/**
* 数组,存储队列元素
*/
private Object[] array;
/**
* 构造函数
*/
public ArrayQueue() {
array = new Object[DEFAULT_SIZE];
}
public ArrayQueue(int size) {
array = new Object[size];
}
/**
* 返回队列的大小
*
* @return
*/
public int size() {
return size;
}
/**
* 添加元素到尾部
*
* @param t
*/
public void add (T t) {
array[size++] = t;
}
/**
* 是否空队列
*
* @return
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 返回队头元素
*
* @return
*/
public T peek() {
return (T)array[0];
}
/**
* 返回并删除队头元素,从队头开始
*
* @return
*/
public T pop() {
T t = (T)array[0];
size--;
for (int i = 0; i < size; i ++) {
array[i] = array[i+1];
}
return t;
}
public static void main(String[] args) {
ArrayQueue<Integer> queue = new ArrayQueue<>(6);
queue.add(0);
queue.add(1);
queue.add(2);
queue.add(3);
queue.add(4);
queue.add(5);
int size = queue.size();
System.out.println("队列大小:" + size);
System.out.println("是否空队列:" + queue.isEmpty());
System.out.println("队头:" + queue.peek());
for (int i = 0; i < size; i ++) {
Integer n = queue.pop();
System.out.println(n);
}
}
}
4. Java中的队列
在Java中,有Queue接口,在Java SE5中仅有两个实现:LinkedList和PriorityQueue。它们的差异在于排序而不是性能。在LinkedList中包含支持双向队列的方法。但在Java类库中没有任何显式的用于双向队列的接口。因此LinkedList无法去实现这样的接口,你也无法转型到Queue那样向上转型到Deque。
4.1 Queue接口
在Java中,提供了Queue接口,在java.util包下,该接口继承自Collection。它的父级接口有Collection、Iterable,已知的子类AbstractQueue, ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedList, PriorityBlockingQueue,PriorityQueue, SynchronousQueue。除了基本的Collection操作外,还提供了其它的插入、提取、检查等工作,每个方法存在两种形式:一种是在操作失败时抛出异常,另一种是返回一个特殊值(null或false,具体返回什么取决于操作)。它有6个方法,如下图。
其实Java也提供了一个Queue类,在java.awt包下,跟我们讨论的队列使用差别比较远,它主要用于awt,所以这里不做说明。
抛出异常 | 返回特殊值 | |
插入 | add(e) | offer(e) |
移除 | remove() | poll() |
检查 | element() | peek() |
方法的说明如下。
4.1.1 添加add(e)
add(e):将一个元素添加到队列中,成功返回true,失败返回false。若没有可用空间则抛出IllegalStateException异常,若类型不匹配则抛出ClassCastException异常,若元素为空则抛出NullPointerException异常,若元素的某些属性非法则抛出IllegalArgumentException异常,一共是四个异常。在AbstractQueue中此方法实际上调用的是offer()方法。
4.1.2 添加offer(e)
offer(e):向队列添加一个元素,成功返回true,失败返回false。若类型不匹配则抛出ClassCastException异常,若元素为空则抛出NullPointerException异常,若元素的某些属性非法则抛出IllegalArgumentException异常。与add()的区别是少抛出了IllegalStateException异常,总共是三个异常,add()是四个。通常此方法比add()好。
4.1.3 删除元素remove()
remove():从队列中删除一个元素,并返回所删除的元素,若队列为空则抛出NoSuchElementException异常。
4.1.4 删队头除poll()
poll():从队列中删除队头,并返回所删除的元素,队列为空时返回null。
4.1.5 返回队头element()
element():不移除元素的情况下返回队头,若队列为空则抛出NoSuchElementException异常。
4.1.6 返回队头peek()
peek():不移除元素的情况下返回队头,若队列为则返回null。
4.2 优先级队列PriorityQueue
先进先出描述了最典型的队列规则。队列规则是指在给定一组队列中的元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个元素应该等待时间最长的元素。
优先级队列声明了下一个弹出队列是最需要的元素,具有最高的优先级。例如,在飞机场,当飞机临近起飞时,这架飞机的乘客可以在办理登机手续时排到队头。如果构建了一个消息系统,某些消息比其他消息更重要,因而应该更快的得到处理,那么它们何时得到处理就与它们何时到达有关。PriorityQueue提供了这种行为的一种自动实现。
PriorityQueue自Java SE5开始添加到Java中,它继承了抽象类AbstractQueue。AbstractQueue又继承自抽象类AbstractCollection,并实现了Queue接口,AbstractCollection实现了Collection接口。
当调用offer()方法插入一个元素,这个元素会在优先级队列中被进行排序。然而这其实还依赖于具体实现,优先级队列算法通常会在插入时排序,排序是基于一个优先级堆,所以需要维护一个堆,但它们也可能在移除时选择最重要的元素。若元素的优先级在它在队列中等待时可以进行修改,那算法的选择就显得很重要了。排序可以自然排序,默认就是自然排序(比如使用new PriorityQueue(int)构造函数创建一个实例时,实际上使用了Comparable的compareTo()方法进行比较排序的),也可以提供自己的Comparator来修改排序(看接下来的构造函数就明白了)。实际上,PriorityQueue提供了Comparator进行排序。在源码中提供了一个Comparator,但是否使用还要看使用者是实现操作的。源码如下:
private final Comparator<? super E> comparator;
其构造函数如下,有多个构造函数,我只展示了下面的三个。
- 第一个构造函数: 两个参数分别是队列容量、排序的比较器。如果使用者定义了比较器并传入,那么就会使用比较器进行排序;
- 第二个构造函数:参数只有队列容量,此时没有比较器,默认使用自然排序,会在添加时会将添加的元素转型为Comparable,所以就是使用了Comparable的compareTo()进行比较排序的,具体可以看下面的“队列的扩容章节”的grow()方法源码。
- 第三个构造函数:指定比较器,队列的容量使用的是默认容量。
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
优先级队列不允许null元素。如果添加的元素是空,则会抛出空指针异常。
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
PriorityQueue可以确保当调用peek()、poll()和remove()方法时,获取的元素将是队列中优先级最高的元素。
LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此LinkedList可以用作Queue的一种实现。通过将LinkedList向上转型为Queue。
4.3 AbstractQueue
此抽象类提供了某此Queue操作的主要实现。此类中的实现适用于基本实现不允许包含null元素。add()、remove()和element()方法分别基于offer()、poll()和peek()方法,但它们通过抛出异常而不是返回flase或null来表示失败。
其方法的简要说明如下。
构造方法摘要 | |
protected | AbstractQueue() 子类使用的构造方法。 |
方法摘要 | |
---|---|
boolean | add(E e) 将指定的元素插入到此队列中(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用空间,则抛出 IllegalStateException。 |
boolean | addAll(Collection<? extends E> c) 将指定 collection 中的所有元素都添加到此队列中。 |
void | clear() 移除此队列中的所有元素。 |
E | element() 获取但不移除此队列的头。 |
E | remove() 获取并移除此队列的头。 |
扩展此抽象类的实现时,至少须定义一个不允许插入null元素的Queue.offer(E)方法,该方法以及 Queue.peek()
、Queue.poll()
、Collection.size()
和 Collection.iterator()
都支持 Iterator.remove()
方法。通常还要重写其他方法。如果无法满足这些要求,那么可以转而考虑为 AbstractCollection
创建子类。
4.4 BlockingQueue
BlockingQueue是一个阻塞队列,它是一个接口,从Java 1.5开始加入。支持的操作有:在检索队列元素时等待队列变为非空,以及在等待队列存储元素时空间变得可用。
public interface BlockingQueue<E> extends Queue<E> {}
下表中总结了一些方法。
抛出异常 | 特殊值 | 阻塞 | 超时 | |
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查 | element() | peek() | 不可用 | 不可用 |
方法有四种形式,不同的方式处理无法立即完成但可能是在将来的某个时刻得到满足:
- 第一个是抛出异常;
- 第二个是返回特殊值(null或false,具体取决于操作);
- 第三个是无限期的阻塞当前线程,直到操作成功为止;
- 第四个是仅在给定的最大时间限制内,然后放弃。
BlockingQueue 不接受 null 元素。试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。null 被用作指示 poll 操作失败的警戒值。
BlockingQueue 可以是限定容量的。它在任意给定时间都可以有一个 remainingCapacity,超出此容量,便无法无阻塞地 put 附加元素。没有任何内部容量约束的 BlockingQueue 总是报告 Integer.MAX_VALUE 的剩余容量。
BlockingQueue 实现主要用于生产者-使用者队列,但它另外还支持 Collection
接口。因此,举例来说,使用 remove(x) 从队列中移除任意一个元素是有可能的。然而,这种操作通常不 会有效执行,只能有计划地偶尔使用,比如在取消排队信息时。
BlockingQueue 实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的。然而,大量的 Collection 操作(addAll、containsAll、retainAll 和 removeAll)没有 必要自动执行,除非在实现中特别说明。因此,举例来说,在只添加了 c 中的一些元素后,addAll(c) 有可能失败(抛出一个异常)。
BlockingQueue 实质上不支持使用任何一种“close”或“shutdown”操作来指示不再添加任何项。这种功能的需求和使用有依赖于实现的倾向。例如,一种常用的策略是:对于生产者,插入特殊的 end-of-stream 或 poison 对象,并根据使用者获取这些对象的时间来对它们进行解释。
以下是基于典型的生产者-使用者场景的一个用例。注意,BlockingQueue 可以安全地与多个生产者和多个使用者一起使用。
/**
* 生产者
*/
static class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) { queue = q; }
@Override
public void run() {
try {
while(true) {
queue.put(produce());
}
} catch (InterruptedException ex) { }
}
/**
* 生产
* @return
*/
public Object produce() {
return null;
}
}
/**
* 消费者
*/
static class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
@Override
public void run() {
try {
while(true) {
consume(queue.take());
}
} catch (InterruptedException ex) {}
}
/**
* 消费
* @param x
*/
public void consume(Object x) { }
}
public static void main(String[] args) {
BlockingQueue q = new ArrayBlockingQueue(3);
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
内存一致性效果:当存在其他并发 collection 时,将对象放入 BlockingQueue
之前的线程中的操作 happen-before 随后通过另一线程从 BlockingQueue
中访问或移除该元素的操作。
4.5 ArrayBlockingQueue
ArrayBlockingQueue是一个由数组支持的有界阻塞队列(有界是必须传入容量),是AbstractQueue的子类,是一个类。此类按先进先出原则对元素进行排序。队列的头部是在队列中存在时间最长的元素,队列的队尾在队列中存在的时间最短的元素。新的元素插入到队列的尾部,获取元素时从队列的头部开始。
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {}
它是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。
此类支持等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证这种排序。然而,通过公平性(fairness)设置为true而构造的队列允许按照先进先出顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。
使用时可以创建一个对象,此类有三个构造方法,其中容量都是必传的参数。
在线程池中,当正在执行的线程数等于核心线程数时,多余的元素会缓存在此队列中,以等待有空闲的线程时继续执行。当此队列满时,元素再加入队列会失败,会开启新的线程也执行,当线程数达到最大线程数时,再有新的元素加入队列时会报错。
4.6 ConcurrentLinkedQueue
一个基于链表的无界线程安全队列。此队列按照先进先出原则对元素进行排序,队列的头部是队列中时间最长的元素,队列的尾部是队列中时间最短的元素。新元素插入到队列的尾部,从队列的头部获取元素。当多个线程共享访问一个公共的collection时,它是一个恰当的选择。此队列不允许使用null元素。
此队列采用了有效的“无等待(wait - free)”算法,该算法基于Maged M. Michael 和 Michael L. Scott 合著的 Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms 中描述的算法。
需要注意的是,与大多数collection不同,size方法不是一个固定时间的操作。由于这些队列的异步特性,确定当前元素的数据需要遍历这些元素。
内存一致性效果:当存在其他并发collection时,将对象放入ConcurrentLinkedQueue之前的线程中操作 happen-before 随后通过另一线程从 ConcurrentLinkedQueue
访问或移除该元素的操作。
4.7 LinkedBlockingQueue
一个基于链表的、范围任意的阻塞队列。此队列按照先进先出原则对元素进行排序,队列的头部是队列中时间最长的元素,队列的尾部是队列中时间最短的元素。新元素插入到队列的尾部,从队列的头部获取元素。此队列的吞吐量通常要高于基于数组的队列,但在大多数并发应用程序中,其可预知的性能要低。
范围任意是在创建此队列时,其构造方法可以传入容量,也可不传入,若不传入则使用默认容量Integer的最大值。
在Java线程池中,若当前执行的线程数量达到核心线程数量,剩余的元素会在阻塞队列中等待。此时使用此阻塞队列时最大线程池就相当于无效了,每个线程完全独立于其他线程。生产者与消费者使用独立的锁来控制数据的同步,即使在高并发的情况下,也可以并行操作队列中的数据。
4.8 SynchronousQueue
同步阻塞队列,每个插入操作必须等待另一个线程的对应移除操作,反之亦然。同步队列没有任何内部容量,所以size()方法始终返回0,甚至连一个队列的容量都没有。其构造方法不需要输入容量。它不存储元素,在生产者消费者模式中,会直接把任务交给消费者。
public class SynchronousQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {}
它为等待过程中的生产者和消费者线程提供可选的公平策略(默认非公平策略,使用默认构造方法创建时),此时创建对象使用的构造方法为SynchronousQueue(boolean fair),fair为true时,则等待线程以先进先出的顺序竞争访问,否则顺序是未指定的。非公平模式通过栈(后进先出)实现,公平模式通过队列实现,请看下面的源码就知道了。使用的数据结构是双重队列(Dual queue)和双重栈(Dual stack),FIFO通常用于支持更高的吞吐量,LIFO则支持更高的线程局部存储(TLS)。
public SynchronousQueue() {
this(false);
}
// TransferQueue和TransferStack是此类的两个内部类
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
不能在同步队列上进行peek(),因为仅在试图要移除元素,该元素才存在;除非另一个线程试图移除某个元素,否则也不能(使用任何方法)插入元素;可理解它的插入和移除是“一对”对称的操作。也不能迭代队列,因为其中没有元素可用于迭代。队列的头是尝试添加到队列中的首个已排队插入线程的元素;若没有这样的已排队线程,则没有可用于移除的元素且poll()将返回null。对于其他Collection方法(如contains()),SynchronousQueue作为一个空collection。此队列不允许null元素。
它像生产者与消费者的会合通道,比较适合“切换”或“传递”这两种场景:一个线程必须同步等待另一个线程把相关信息或时间或任务传递给它。就是说当生产者生产出的任何,会直接交给消费者,而不会放入到队列中。若是使用ArrayBlockingQueue,队列中可能会有任何的。
它的一个典型应用是在线程池中,Executors.newCachedThreadPool()就使用了它,这个构造使线程池根据需要(新的任务到来时)创建新的线程,若有空闲线程则会重复使用,线程空闲60秒后会被回收。
在下面的示例中,使用了此队列,当调用add()方法,会直接报错:Queue full。使用了offer方法,但内容是空的。
public static void main(String[] args) {
SynchronousQueue<String> queue = new SynchronousQueue();
// 使用offer方法,不会报错
queue.offer("1");
// 使用add方法,会报错:Queue full
queue.add("1");
System.out.println("大小:" + queue.size());
//
queue.forEach(item -> System.out.println(item.toString()));
}
在生产者消费者模式中,使用示例如下。
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>();
new Producer(queue).start();
new Consumer(queue).start();
}
/**
* 生产者类
*/
static class Producer extends Thread{
private SynchronousQueue<Integer> queue;
public Producer(SynchronousQueue<Integer> queue){
this.queue = queue;
}
@Override
public void run(){
// for(;;){
for (int i = 0; i < 5; i ++) {
int random = new Random().nextInt(100);
System.out.println("生产一个产品:" + random);
// 休眠三秒后执行
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
queue.put(random);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产者队列是否空:" + queue.isEmpty());
}
}
}
/**
* 消费者类
*/
static class Consumer extends Thread{
private SynchronousQueue<Integer> queue;
public Consumer(SynchronousQueue<Integer> queue){
this.queue = queue;
}
@Override
public void run(){
for(;;){
try {
System.out.println("消费一个产品:" + queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------------------- 我是分割线 -----------------------");
}
}
}
}
4.9 DelayQueue
Delayed元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size 方法同时返回到期和未到期元素的计数。此队列不允许使用 null 元素。
public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> {}
4.10 Java中队列的扩容
在PriorityQueue优先级队列中,其默认容量是11。在扩容时,如果容量很小则扩展一倍,否则扩展原来容量的一半,请看grow()两个方法的源码。在扩容时,如果原来的容量小于64,则在原来容量的基础上增加一倍再加2,否则就增加一半(oldCapacity >> 1)。
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
5 循环队列(Circular Queue)
循环队列是头尾相接的顺序存储结构的形式。对于顺序存储结构的队列,使用数组实现队列。为了避免数组插入和删除时需要移动数据,于是引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是O(n)的时间复杂度变成了O(1)。
循环队列的引队,主要是为了解决队列的插入删除复杂度以及假溢出问题(下文会有描述)。
6 双端队列
6.1 双端队列
6.1.1 双端队列(Deque)
双向队列(双端队列)是允许可以在队列的任何一端添加或移除元素的队列。逻辑结构上仍是线性结构。将队列的两端分别称为前端和后端,两端都可以入队和出队。
在双端队列入队时,前端入队的元素排在队列中后端入队的元素的前面。后端入队的元素排在队列中前端入队的元素的后面。在双端队列出队时,无论是前端还是后端出队,先出队的元素排在后出队的元素的前面。
6.1.2 输入受限的双端队列
输入受限的双端队列,是允许在一端进行插入和删除,但在另一端只允许删除的双端队列。如果限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就变为两个栈底相邻的栈了。
6.1.3 输出受限的双端队列
输出受限的双端队列,是允许在一端进行插入和删除,但在另一端插入的双端队列。
6.3.4 双端队列的输入输出例题分析
假设有一个双端队列,输入序列为1、2、3、4。试分别求出以下条件的输出序列。
- 能由输入受限的双端队列得到,但不能由输出受限的双端队列得到的输出序列;
- 能由输出受限的双端队列得到,但不能由输入受限的双端队列得到的输出序列;
- 既不能由输入受限的双端队列得到,也不能由输出受限的双端队列得到的输出序列。
先看输入受限的双端队列,前端为end1,后端为end2。假设end1端输入1、2、3、4,那么end2端的输出相当于队列的输出:1、2、3、4;而end1端的输出相当于栈的输出,当n = 4时仅通过end1端有14种输出序列(由Catalan【卡塔兰数是组合数学中一个常在各种计数问题中出现的数列,一般公式为】公式计算得出:8!/(5! * 4!) = 14),仅通过end1不能得到的输出序列有4! - 14 = 10种,它们分别是:
1、4、2、3 | 2、4、1、3 | 3、4、1、2 | 3、1、4、2 | 3、1、2、4 |
4、3、1、2 | 4、1、3、2 | 4、2、3、1 | 4、2、1、3 | 4、1、2、3 |
通过end1和end2端混合输出,可以输出这10种中的8种,参看下表,其中SL、XL分别代表end1端的入队和出队,XR代表end2端的出队。如下图。剩下两种是不能通过输入受限的双端队列输出的,它们是4、2、3、1和4、2、1、3。
再看输出受限的双端队列,前端为end1,后端为end2。假设end1端和end2端都能输入,仅end2端可以输出,若都从end2端输入,从end2端输出,就是一个栈了。当输入序列为1、2、3、4时,输出序列有14种,对于其他10(24-14=10)种不能得到的输出序列,通过交替从end1和end2端输入,还可以输出其中的8种。假设SL代表end1端的入队,SR和XR代表end2端的入队和出队,则可能的输出序列及入队和出队顺序见下图。
通过输出受限的双端队列不能得到的两种输出序列是4、1、3、2和4、2、3、1。
综上所述,可以知道:
- 能由输入受限的双端队列得到,但不能由输出受限的双端队列得到的是4、1、3、2;
- 能由输出受限的双端队列得到,但不能由输入受限的双端队列得到的是4、2、1、3;
- 既不能由输入受限的双端队列得到,又不能由输出受限的双端队列得到的是4、2、3、1。
6.2 Java中的双端队列
6.2.1 Deque
在Java中,LinkedList中包含了支持双向队列的方法,但在Java标准类库中没有任何显式的用于双端队列的接口。因此,LinkedList无法去实现这样的接口。你也无法像转型到Queue那样去向上转型到Deque。但可以使用组合来创建一个Deque类,并直接从LinkedList中暴露相关的方法。
从Java1.6开始,提供了Deque接口。它继承了Queue接口(下方有介绍),其所有父类接口有Queue、Collection和Iterable。已知的子类有BlockingDeque,已知的实现类有ArrayDeque、LinkedBlockingDeque、LinkedList。大多数Deque实现对于它们能够包含的元素数量没有固定的限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。
此接口与List不同,它不支持通过索引访问元素。Deque实现通常不定义基于元素的equals和hashcode方法,而是从Object类继承基于身体的equals和hashcode方法。
Deque的方法
此接口提供插入、移除和检查元素的方法,每种方法都有两种形式:一种是在操作失败时抛出异常,另一种形式是返回一个特殊值(null或false,到底返回哪处取决于具体的操作)。插入操作的后一种形式是专门为使用有容量限制的Deque实现设计的,在大多数实现中,插入操作不能失败,如下表。
第一个元素(头部) | 最后一个元素(尾部) | |||
抛出异常 | 特殊值 | 抛出异常 | 特殊值 | |
插入 | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
移除 | removeFirst() | pollFirst() | removeLast() | pollLast() |
检查 | getFirst() | peekFirst() | getLast() | peekLast() |
Deque的方法的具体说明可以参见https://tool.oschina.net/uploads/apidocs/jdk-zh/java/util/Deque.html。
Deque的代码方法截图如下。
Deque的使用
此接口扩展了Queue接口。在将双端队列用作队列时,将得到先进先出行为。将元素添加到双端队列的尾部,从双端队列的开头移除元素。从Queue接口继承的方法完全等效于Deque方法,如下表所示。
Queue 方法 | 等效 Deque 方法 |
add(e) | addLast(e) |
offer(e) | offerLast(e) |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
在双端队列也可以用作后进先出的栈。在使用时应该优先使用此接口而不是使用java遗留的Stack类(此类在数据结构与算法(五)—— 栈及其实现和应用有一定的介绍)。在将双端队列用做栈时,元素被推入双端队列的开头并从双端队列的开头弹出。栈方法完全等效于Deque接口,如下表所示。
堆栈方法 | 等效 Deque 方法 |
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
6.2.2 BlockingDeque
BlockingDeque是阻塞的双端队列,在java.util.concurrent包下,从Java 1.6开始出现。
public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E>{}
获取元素时等待双端队列变为非空,存储元素时等待双端队列中的空间变得可用。其方法有四种形式,使用不同的方式处理无法立即满足但在将来某一时刻可能满足的操作:
- 第一种方式抛出异常;
- 第二种返回一个特殊值(null或false,具体取决于操作);
- 第三种无限期阻塞当前线程,直到成功;
- 第四种只阻塞给定的最大时间,然后放弃。
第一个元素(头部) | ||||
抛出异常 | 特殊值 | 阻塞 | 超时期 | |
插入 | addFirst(e) | offerFirst(e) | putFirst(e) | offerFirst(e, time, unit) |
移除 | removeFirst() | pollFirst() | takeFirst() | pollFirst(time, unit) |
检查 | getFirst() | peekFirst() | 不适用 | 不适用 |
最后一个元素(尾部) | ||||
抛出异常 | 特殊值 | 阻塞 | 超时期 | |
插入 | addLast(e) | offerLast(e) | putLast(e) | offerLast(e, time, unit) |
移除 | removeLast() | pollLast() | takeLast() | pollLast(time, unit) |
检查 | getLast() | peekLast() | 不适用 | 不适用 |
像所有Blocking一样,BlockingDeque是线程安全的,但不允许null元素,且可能有(也可能没有)容量限制。
BlockingDeque实现可以直接用作先进先出的BlockingQueue。继承自BlockingQueue的方法精确的等效于下表中描述的BlockingDeque方法:
内容一致性效果:当存在其他并发collection时,将对象放入BlockingDeque之前的线程中操作happen-before随后通过另一线程从BlockingDeque中访问或移除该元素的操作。
6.2.3 LinkedBlockingDeque
它是一个基于链表、任选范围的阻塞双端队列。位于java.util.concurrent包下,继承自AbstractQueue,实现了BlockingDeque。任选范围是在创建此对象时,容量可输也可不输,不输入则默认为Integer的最大值。
public class LinkedBlockingDeque<E> extends AbstractQueue<E> implements BlockingDeque<E>, java.io.Serializable {}
因为是基于链表,在插入元素时,只要不超出容量,都将动态的创建链接节点。大多数操作都以固定时间运行(不计阻塞消耗的时间)。