2

引言

本周在编写短信验证码频率限制切面的时候,经潘老师给的实现思路,使用队列进行实现。

看了看java.util包下的Queue接口,发现还从来没用过呢!

Collection集合类接口,由它派生出ListSetQueueMap属于另一个独立的接口,和Collection没有继承关系。

clipboard.png

ListSetMap我们用的都是已经相当熟练了,今天,我们就来学习这个队列Queue

探索

队列与栈都是数据结构的基础话题,队列:先进先出;栈:后进先出。

方法

Queue接口中声明了六个方法,分成三对来使用。

clipboard.png

入队操作

方法 特点 建议
add 入队失败抛出异常
offer 入队失败返回false 推荐

出队操作

方法 特点 建议
remove 出队失败抛出异常
poll 出队失败返回null 推荐

取队头操作

方法 特点 建议
element 队列为空时抛出异常
peek 队列为空时返回null 推荐

PriorityQueue

java.util包中,除抽象类外,直接实现Queue接口的只有PriorityQueue优先级队列。

优先级队列比普通的队列要高级,普通的队列如果是先进的肯定是在队头的,而优先级队列根据优先级判断当前队头元素是什么。很适合实现操作系统中的按优先级实现进程调度。

clipboard.png

如果需要使用优先级队列进行排序时,需要传入比较器。

该队列使用数组实现,线程不安全。

Deque

clipboard.png

java.util包中,Deque接口继承Queue接口。

Dequedouble-ended queue,双端队列。

双端队列,相比普通队列就是可操作两端,有两个队头,也有两个队尾。

clipboard.png

所以再去看Deque接口中声明的方法,都是两套的。offerFirstofferLastpollFirstpollLast等。

所以说,如果使用双端队列,不仅可以当队列用,也可以当栈用,因为可以自己控制出的是队头还是队尾。

Deque有两个实现类:ArrayDequeLinkedList

原来LinkedList不仅实现了List接口,还实现了Deque接口。

clipboard.png

两者的区别显而易见,一个是数组方式实现的,一个是链表的方式实现的。

BlockingQueue

这些都是java.util包下的,都是线程不安全的实现,JDK所有线程安全的队列实现都在java.util.concurrent包下,也就是阻塞队列BlockingQueue

clipboard.png

concurrent包下,自然是做了线程安全处理的了,在多线程环境下操作队列需要使用。

生产者消费者

与阻塞队列最密切的就是生产者消费者模型了,我们一起来探讨一下。

生产者消费者模型,最初出现在操作系统中,多进程/多线程进行协作,完成同一任务,必然需要相互合作与相互制约。

clipboard.png

举一个符合实际的例子,我想喝可乐。

clipboard.png

可口可乐公司就是生产者,用于生产商品。

超市就相当于缓冲区,用于存储生产者生产出来的可乐,公司生产出可乐,然后放到超市里卖。

我就是消费者,去超市买可乐(消费过程)。

所以就会有一个同步的问题:

假设场景:超市能容量100瓶可乐。

所以,消费者去购买的前提是:超市内有可乐,要不去了也买不着。

生产者生产的前提是:超市内有空余位置,要不生产了往哪送呢?

类比到程序设计中,就是进程或线程之间的相互制约,也就是所谓的同步!

线程类比

一图胜千言,我就不赘述了。

clipboard.png

消费者线程想去找缓冲区要数据,先判断缓冲区内有没有数据,如果没有,消费者就拿不到,这个线程就等待,直到:缓冲区内有数据。如果有,就从缓冲区将数据拿走。

生产者线程要去生产数据,先判断缓冲区内有没有空余位置,如果没有,生产者就等待,直到:缓冲区内有空位,如果有,就生产数据,放入缓冲区。

阻塞队列

阻塞队列正适合生产者消费者模型,当队列满时,入队操作就会被阻塞,当队列空时,出队操作就会被阻塞。

clipboard.png

入队出队的offer方法和poll方法与原队列接口的方法相比,多了时间的参数。当发生阻塞时,如果超过了设置的时间,线程就会退出,毕竟如果最坏的情况,一直不满足条件,也不能一直阻塞下去。

boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
E poll(long timeout, TimeUnit unit) throws InterruptedException;

实现类

ArrayBlockingQueue:数组实现的阻塞队列。

LinkedBlockingQueue:链表实现的阻塞队列。

PriorityBlockingQueue:优先级阻塞队列。

双向阻塞队列

这个简单,就是同时实现了BlockingQueueDeque接口。

java.util.concurrent包下只有一个双向阻塞队列的实现:LinkedBlockingDeque

延时队列

延时队列:DelayQueue,看这个类名,无疑了,此队列定与时间有关。

当一个元素入队时,它并不是马上进入队列,而是根据设定的时间延时之后再入队。

假设offer一个元素,设置时间为10s,在10s内访问队列,是访问不到元素的。

在延时之后,也就是10s之后,再去访问,该元素才在队列中。

使用场景

相关使用场景就是定时缓存。

HashMapDelayQueue配合使用。用DelayQueue来存储缓存的key,如果队列中有元素,表示该key就已经过期。

然后再建一个线程去清理缓存,执行到poll方法时,使用不传时间的方法,如果队列为空,该线程就一直阻塞在这,不往下走。

队列中有元素时,就说明有key过期了,线程继续执行,然后元素出队,根据相应的key移除缓存。

细节

clipboard.png

延时队列中存储的元素需要实现Delayed接口。

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

getDelay方法返回剩余的延时时间,如果返回值大于0,表示还未到入队时间。

同步队列

SynchronousQueue:同步队列。

clipboard.png

最好的解释自然是官方文档:A BlockingQueue in which each insert operation must wait for a corresponding remove operation by another thread, and vice versa.

这是一个阻塞队列,它的特点是在执行插入操作时必须等待另一个线程的移除操作。什么意思呢?

clipboard.png

通俗的来说就是买可乐不需要去超市了,我(消费者)直接和厂家(生产者)购买。

所以,生产者和消费者同时存在时,这个交易才能执行,两方达成约定后,生产者生产可乐,卖给消费者。缺少任何一方另一方都会被阻塞,条件满足时会唤醒对方继续执行,这就是所谓的同步。

代码层面讲就是:puttake方法都被调用的时候,两者才开始执行,并完成了数据的传递。

所以严格来说,虽然SynchronousQueue实现了队列接口,但是它的目的却并不是队列,而是将生产者消费者线程配对。

转移队列

LinkedTransferQueue:链式转移队列。虽然放在了最后,但是查阅相关文档发现,实际的生产环境中,这个队列最常用。

怎么转移的呢?

消费者找队列拿数据,如果没有数据可用,就设置一个标志位,表示我这里期待着一个数据,然后消费者就开始等。

等着等着,直到生产者来了,判断,如果有等着的,就直接把数据给它,实现了数据转移。如果没有呢?就去执行数据入队相关的操作。

总结

clipboard.png

点开了阻塞队列的源码,发现线程安全是使用锁实现的。

再看看面试问的东西:乐观锁、悲观锁、自旋锁、偏向锁、公平锁,这都是写啥东西呀?

吾生也有涯,而Java无涯。

clipboard.png


张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。