本篇主要内容如下:
本篇主要内容
帮你总结好的阻塞队列:
18种Queue总结
队列原理图
hi,大家好,我的英文名叫Queue
,中文名叫队列
,无论现实生活中还是计算机的世界中,我都是一个很重要的角色哦~
我是一种数据结构
,大家可以把我想象成一个数组,元素从我的一头进入、从另外一头出去,称为FIFO原则(先进先出原则)。
我还有两个亲兄弟:List
(列表)、Set
(集),他们都是Collection
的儿子,我还有一个远房亲戚:Map
(映射)。他们都是java.util
包这个大家庭的成员哦~
18种队列分为三大类: 接口、抽象类、普通类。
弄清楚下面的继承实现关系对后面理解18种队列有很大帮助。
18个Queue的继承实现关系图
Queue
接口继承Collection
接口,Collection
接口继承Iterable
接口BlockingQueue
接口、Deque
接口 继承Queue
接口AbstractQueue
抽象类实现Queue
接口BlockingDeque
接口、TransferQueue
接口继承BlockingQueue
接口BlockingDeque
接口继承Deque
接口LinkedBlockingDeque
类实现BlockingDeque
接口LinkedTransferQueue
类接口实现TransferQueue
接口LinkedList
类、ArrayDeque
类、ConcurrentLinkedDeque
类实现 了Deque
接口ArrayBlockingQueue
类、LinkendBlockingQueue
类、LinkedBlockingDeque
类、LinkedTransferQueue
类、SynchronousQueue
类、PriorityBlockQueue
类、DelayQueue类
继承 了AbstractQueue
抽象类和实现了BlockingQueue接口PriorityQueue
类和ConcurrentLinkedQueue
类继承 了AbstractQueue
抽象类注意:
总共有3组方法,每一组方法对应两个方法。如下图所示:
Queue的核心方法
动作 | 抛异常 | 返回特殊值 |
---|---|---|
Insert | add(e) | offer(e) |
Remove | remove() | poll |
Examine | element() | peek() |
添加(Insert)
元素的动作,会有两种方式:add(e)
和offer(e)
。如果调用add(e)方法时,添加失败,则会抛异常
,而如果调用的是offer(e)方法失败时,则会返回false
。offer方法用于异常是正常的情况下使用,比如在有界队列中,优先使用offer方法。假如队列满了,不能添加元素,offer方法返回false,这样我们就知道是队列满了,而不是去handle运行时抛出的异常。双端队列Deque
(1)Deque概念: 支持两端元素插入和移除的线性集合。名称deque
是双端队列的缩写,通常发音为deck
。大多数实现Deque的类,对它们包含的元素的数量没有固定的限制的,支持有界和无界。
(2)Deque方法说明:
Deque方法
说明:
比如Queue的add方法和Deque的addLast方法等价。
Deque也可以用作LIFO(后进先出)栈,这个接口优于传统的Stack类。当作为栈使用时,元素被push到deque队列的头,而pop也是从队列的头pop出来。
Stack(栈)的方法正好等同于Deque的如下方法:
注意:peek方法不论是作为栈还是队列,都是从队列的检测队列的头,返回最先加入的元素。比如第一次put 100,第二次put 200,则peek返回的是100。如下图所示:
示例代码
AbstractQueue是一个抽象类,继承了Queue接口,提供了一些Queue操作的骨架实现。
AbstractQueue的方法
方法add、remove、element方法基于offer、poll和peek。也就是说如果不能正常操作,则抛出异常。我们来看下AbstactQueue是怎么做到的。
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
注意:
ArrayBlockingQueue
类、LinkendBlockingQueue
类、LinkedBlockingDeque
类、LinkedTransferQueue
类、SynchronousQueue
类、PriorityBlockQueue
类、DelayQueue类
继承 了AbstractQueue
抽象类PriorityQueue
类和ConcurrentLinkedQueue
类继承 了AbstractQueue
抽象类阻塞队列满了的情况
阻塞队列为空的情况
(1)BlockingQueue(阻塞队列)也是一种队列,支持阻塞的插入和移除方法。
(3)阻塞的插入:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
(4)阻塞的移除:当队列为空,获取元素的线程会等待队列变为非空。
(5)应用场景:生产者和消费者,生产者线程向队列里添加元素,消费者线程从队列里移除元素,阻塞队列时获取和存放元素的容器。
(6)为什么要用阻塞队列:生产者生产和消费者消费的速率不一样,需要用队列来解决速率差问题,当队列满了或空的时候,则需要阻塞生产或消费动作来解决队列满或空的问题。
线程A往阻塞队列(Blocking Queue)中添加
元素,而线程B从阻塞队列中移除
元素。
当阻塞队列为空的时候 (一个元素都没有),则从队列中获取元素的操作将会被阻塞。
生活中的案例:去海底捞吃火锅的时候,早上8点没人来吃火锅,所以需要等客人过来。
翻译成线程:现在没有元素需要添加,而且阻塞队列为空,所以线程B不需要从队列中拿元素出来,所以线程B获取元素的操作被阻塞了。
当阻塞队列满了的时候 (所有位置都放有元素),则从队列中添加元素的操作将会被阻塞。
生活中的案例:去海底捞吃火锅的时候,人太多了,需要排号,等其他桌空出来了才能进去。
翻译成线程:线程A往阻塞队列中添加元素,将队列填满了,线程B现在正在忙,无法拿出队列中的元素,所以阻塞队列没有地方再放元素了,这个时候线程A添加元素的操作就被阻塞了
BlockingQueue接口的10个核心方法:
继承的方法
10个核心方法总结如下:
BlockingQueue接口的10个核心方法
有三大类操作:插入、移除、检查。
插入有四种方法: add、offer、put、offer超时版。
IllegalStateException
- 队列满了
ClassCastException
- 添加的元素类型不匹配
NullPointerException
- 添加的元素为null
IllegalArgumentException
- 添加的元素某些属性不匹配
add方法特别之处用于添加失败时抛出异常,共有四种异常:
offer方法特别之处用于添加失败时只返回false
put方法特别之处用于当阻塞队列满时,生产者如果往队列里put元素,则队列会一直阻塞生产者线程,直到队列可用或者响应中断退出
offer超时方法特别之处用于当阻塞队列满时,生产者如果往队列里面插入元素,队列会阻塞生产者线程一段时间,如果超过了指定时间,生产者线程会退出,并返回false。
移除有四种方法: remove、poll、take、poll超时版
NoSuchElementException
- 如果这个队列是空的
remove方法特别之处用于移除失败时抛出异常
poll方法特别之处用于移除失败时返回null
take方法特别之处用于当阻塞队列为空时,消费者线程如果从队列里面移除元素,则队列会一直阻塞消费者线程,直到队列不为空
poll超时方法特别之处用于当阻塞队列空时,消费者如果从队列里面删除元素,则队列会一直阻塞消费者线程,如果超过了指定时间,消费者线程会退出,并返回null
检查有两种方法: element、peek
element方法用于检测头部元素的存在性,如果队列为空,则抛出异常,否则返回头部元素。
peek方法用于检测头部元素的存在性,如果队列为空,返回特殊值null,否则返回头部的元素。
BlockingDeque满了
BlockQueue为空
是阻塞队列BlockingQueue
和双向队列Deque
接口的结合。有如下方法:
BlockingDeque接口方法
示例:
尝试执行以下方法:
LinkedBlockingDeque queue = new LinkedBlockingDeque();
queue.addFirst("test1");
queue.addFirst(300);
queue.addLast("400");
最后队列中的元素顺序如下:
300, test1, 400。
先添加了test1放到队列的头部,然后在头部的前面放入300,所以300在最前面,成为头部,然后将400放入队列的尾部,所以最后的结果是300, test1, 400。
队列种的元素
BlockDeque和BlockQueue的对等方法
如果有消费者正在获取元素,则将队列中的元素传递给消费者。如果没有消费者,则等待消费者消费。我把它称作使命必达队列,必须将任务完成才能返回。
针对TransferQueue的transfer方法
圆通快递员要将小明的2个快递送货到门,韵达快递员也想将小明的2个快递送货到门。小明一次只能拿一个,快递员必须等小明拿了一个后,才能继续给第二个。
针对TransferQueue的tryTransfer方法
圆通快递员要将小明的2个快递送货到门,韵达快递员也想将小明的2个快递送货到门。发现小明不在家,就把快递直接放到菜鸟驿站了。
针对TransferQueue的tryTransfer超时方法
圆通快递员要将小明的2个快递送货到门,韵达快递员也想将小明的2个快递送货到门。发现小明不在家,于是先等了5分钟,发现小明还没有回来,就把快递直接放到菜鸟驿站了。
transfer(E e)
原理如下图所示:
transfer方法的原理
原理图解释:生产者线程Producer Thread尝试将元素B传给消费者线程,如果没有消费者线程,则将元素B放到尾节点。并且生产者线程等待元素B被消费。当元素B被消费后,生产者线程返回。
如果当前有消费者正在等待接收元素(消费者通过take方法或超时限制的poll方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。
如果没有消费者等待接收元素,transfer方法会将元素放在队列的tail(尾)节点,并等到该元素被消费者消费了才返回。
tryTransfer(E e)
试探生产者传入的元素是否能直接传给消费者。
如果没有消费者等待接收元素,则返回false。
和transfer方法的区别是,无论消费者是否接收,方法立即返回。
tryTransfer(E e, long timeout, TimeUnit unit)
带有时间限制的tryTransfer方法。
试图把生产者传入的元素直接传给消费者。
如果没有消费者消费该元素则等待指定的时间再返回。
如果超时了还没有消费元素,则返回false。
如果在超时时间内消费了元素,则返回true。
getWaitingConsumerCount()
获取通过BlockingQueue.take()方法或超时限制poll方法等待接受元素的消费者数量。近似值。
返回等待接收元素的消费者数量。
hasWaitingConsumer()
获取是否有通过BlockingQueue.tabke()方法或超时限制poll方法等待接受元素的消费者。
返回true则表示至少有一个等待消费者。
本应该按照升序排序
按照自定义优先级排序
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
public Comparator<? super E> comparator() {
return comparator;
}
Arrays.sort(pq.toArray)
。offer
、add
)和出列( poll
、remove()
)的时间复杂度O(log(n))。LinkedList的结构
我们来看下节点类Node
private static class Node<E> {
E item; //元素
Node<E> next; //向后的节点链接
Node<E> prev; //向前的节点链接
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
1.LinkedList的增加和删除效率相对较高,而查找和修改的效率相对较低。
2.以下情况建议使用ArrayList
频繁访问列表中的一个元素。
只在列表的首尾添加元素。
3.以下情况建议使用LinkedList
频繁地在列表开头、中间、末尾添加和删除元素。
需要通过循环迭代来访问列表中的元素。
LinkedList不是线程安全的,所以可以使用如下方式保证线程安全。
List list = Collections.synchronizedList(new LinkedList<>());
ConcurrentLinkedQueue原理
ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
BuildingBlockWithName buildingBlock = new BuildingBlockWithName("三角形", "A");
concurrentLinkedQueue.add(buildingBlock);
ArrayDeque原理图
创建一个ArrayDeque,往arrayDeque队尾添加元素。
ArrayDeque arrayDeque = new ArrayDeque();
for (int i = 0; i < 50; i++) {
arrayDeque.add(buildingBlock); // add方法等价于addLast方法
}
ConcurrentLinkedDeque原理图
创建两个积木:三角形、四边形,然后添加到队列:
BuildingBlockWithName buildingBlock1 = new BuildingBlockWithName("三角形", "A");
BuildingBlockWithName buildingBlock2 = new BuildingBlockWithName("四边形", "B");
ConcurrentLinkedDeque concurrentLinkedDeque = new ConcurrentLinkedDeque();
concurrentLinkedDeque.addFirst(buildingBlock1);
concurrentLinkedDeque.addLast(buildingBlock2);
//结果:顺序:三角形、四边形
ArrayBlockingQueuey原理图
创建两个积木:三角形、四边形,然后添加到队列:
BuildingBlockWithName buildingBlock1 = new BuildingBlockWithName("三角形", "A");
BuildingBlockWithName buildingBlock2 = new BuildingBlockWithName("四边形", "B");
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(100, true);
arrayBlockingQueue.add(buildingBlock1);
arrayBlockingQueue.add(buildingBlock2);
LinkedBlockingQueue原理
LinkedList linkedList1 = new LinkedList();
linkedList1.add("A");
linkedList1.add("B");
linkedList1.add("C");
LinkedBlockingDeque原理图
LinkedBlockingDeque可以用在“工作窃取“模式中。
工作窃取算法
:某个线程比较空闲,从其他线程的工作队列中的队尾窃取任务来帮忙执行。
LinkedTransferQueue原理图
LinkedTransferQueue = 阻塞队列+链表结构+TransferQueue
之前我们讲“使命必达TransferQueue接口时已经介绍过了TransferQueue接口 ,所以LinkedTransferQueue接口跟它相似,只是加入了阻塞插入和移除的功能,以及结构是链表结构。
之前的TransferQueue也讲到了3个案例来说明TransferQueue的原理,大家可以回看TransferQueue。
生产者1 依次往队列中添加 A、B、C
生产者2 依次往队列中添加 D、E、F
消费者消费元素
生产者1 transfer A
生产者2 transfer D
2s后...
消费者 take A
生产者1 put B
2s后...
消费者 take D
生产者2 transfer E
2s后...
消费者 take B
生产者1 transfer C
(1)首先生产者线程1和2 调用transfer方法来传输A和D,发现没有消费者线程接收,所以被阻塞。
(2)消费者线程过了2s后将A拿走了,然后生产者1 被释放继续执行,传输元素B,发现又没有消费者消费,所以进行了等待。
(3)消费者线程过了2s后,将排在队列首部的D元素拿走,生产者2继续往下执行,传输元素E,发现没有消费者,所以进行了等待。
(4)消费者线程过了2s后,将排在队列首部的B元素拿走,生产者1传输C元素,等待消费者拿走。
(5)消费者不再消费了,所以生产者1和生产者2都被阻塞了,元素C和,元素E都没有被拿走,而且生产者2的F元素还没有开始传输,因为在等待元素D被拿走。
(6)看下队列里面确实有C和E元素,而且E排在队列的首部。
队列里面的元素
SynchronousQueue原理图
我们创建了两个线程,一个线程用于生产,一个线程用于消费
生产的线程依次put A、B、C三个值
消费线程每隔5s调用take方法
t1 put A
t2 take A
5秒后...
t1 put B
t2 take B
5秒后...
t1 put C
t2 take C
小结:说明生产线程执行put第一个元素"A" 操作后,需要等待消费者线程take完“A”后,才能继续往下执行代码。
PriorityBlockQueue的原理图
DelayQueue原理图
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
poll方法
这一篇花了很多心思在上面,看官方英文文档、画原理图、写demo代码,排版。这恐怕是市面上最全最细讲解Queue的。
- END -
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8