引言
本周在编写短信验证码频率限制切面的时候,经潘老师给的实现思路,使用队列进行实现。
看了看java.util
包下的Queue
接口,发现还从来没用过呢!
Collection
集合类接口,由它派生出List
、Set
和Queue
,Map
属于另一个独立的接口,和Collection
没有继承关系。
List
、Set
和Map
我们用的都是已经相当熟练了,今天,我们就来学习这个队列Queue
!
探索
队列与栈都是数据结构的基础话题,队列:先进先出;栈:后进先出。
方法
Queue
接口中声明了六个方法,分成三对来使用。
入队操作
方法 | 特点 | 建议 |
---|---|---|
add | 入队失败抛出异常 | |
offer | 入队失败返回false
|
推荐 |
出队操作
方法 | 特点 | 建议 |
---|---|---|
remove | 出队失败抛出异常 | |
poll | 出队失败返回null
|
推荐 |
取队头操作
方法 | 特点 | 建议 |
---|---|---|
element | 队列为空时抛出异常 | |
peek | 队列为空时返回null
|
推荐 |
PriorityQueue
在java.util
包中,除抽象类外,直接实现Queue
接口的只有PriorityQueue
优先级队列。
优先级队列比普通的队列要高级,普通的队列如果是先进的肯定是在队头的,而优先级队列根据优先级判断当前队头元素是什么。很适合实现操作系统中的按优先级实现进程调度。
如果需要使用优先级队列进行排序时,需要传入比较器。
该队列使用数组实现,线程不安全。
Deque
java.util
包中,Deque
接口继承Queue
接口。
Deque
:double-ended queue
,双端队列。
双端队列,相比普通队列就是可操作两端,有两个队头,也有两个队尾。
所以再去看Deque
接口中声明的方法,都是两套的。offerFirst
、offerLast
、pollFirst
、pollLast
等。
所以说,如果使用双端队列,不仅可以当队列用,也可以当栈用,因为可以自己控制出的是队头还是队尾。
Deque
有两个实现类:ArrayDeque
和LinkedList
。
原来LinkedList
不仅实现了List
接口,还实现了Deque
接口。
两者的区别显而易见,一个是数组方式实现的,一个是链表的方式实现的。
BlockingQueue
这些都是java.util
包下的,都是线程不安全的实现,JDK
所有线程安全的队列实现都在java.util.concurrent
包下,也就是阻塞队列BlockingQueue
。
在concurrent
包下,自然是做了线程安全处理的了,在多线程环境下操作队列需要使用。
生产者消费者
与阻塞队列最密切的就是生产者消费者模型了,我们一起来探讨一下。
生产者消费者模型,最初出现在操作系统中,多进程/多线程进行协作,完成同一任务,必然需要相互合作与相互制约。
举一个符合实际的例子,我想喝可乐。
可口可乐公司就是生产者,用于生产商品。
超市就相当于缓冲区,用于存储生产者生产出来的可乐,公司生产出可乐,然后放到超市里卖。
我就是消费者,去超市买可乐(消费过程)。
所以就会有一个同步的问题:
假设场景:超市能容量100
瓶可乐。
所以,消费者去购买的前提是:超市内有可乐,要不去了也买不着。
生产者生产的前提是:超市内有空余位置,要不生产了往哪送呢?
类比到程序设计中,就是进程或线程之间的相互制约,也就是所谓的同步!
线程类比
一图胜千言,我就不赘述了。
消费者线程想去找缓冲区要数据,先判断缓冲区内有没有数据,如果没有,消费者就拿不到,这个线程就等待,直到:缓冲区内有数据。如果有,就从缓冲区将数据拿走。
生产者线程要去生产数据,先判断缓冲区内有没有空余位置,如果没有,生产者就等待,直到:缓冲区内有空位,如果有,就生产数据,放入缓冲区。
阻塞队列
阻塞队列正适合生产者消费者模型,当队列满时,入队操作就会被阻塞,当队列空时,出队操作就会被阻塞。
入队出队的offer
方法和poll
方法与原队列接口的方法相比,多了时间的参数。当发生阻塞时,如果超过了设置的时间,线程就会退出,毕竟如果最坏的情况,一直不满足条件,也不能一直阻塞下去。
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
E poll(long timeout, TimeUnit unit) throws InterruptedException;
实现类
ArrayBlockingQueue
:数组实现的阻塞队列。
LinkedBlockingQueue
:链表实现的阻塞队列。
PriorityBlockingQueue
:优先级阻塞队列。
双向阻塞队列
这个简单,就是同时实现了BlockingQueue
和Deque
接口。
java.util.concurrent
包下只有一个双向阻塞队列的实现:LinkedBlockingDeque
。
延时队列
延时队列:DelayQueue
,看这个类名,无疑了,此队列定与时间有关。
当一个元素入队时,它并不是马上进入队列,而是根据设定的时间延时之后再入队。
假设offer
一个元素,设置时间为10s
,在10s
内访问队列,是访问不到元素的。
在延时之后,也就是10s
之后,再去访问,该元素才在队列中。
使用场景
相关使用场景就是定时缓存。
HashMap
和DelayQueue
配合使用。用DelayQueue
来存储缓存的key
,如果队列中有元素,表示该key
就已经过期。
然后再建一个线程去清理缓存,执行到poll
方法时,使用不传时间的方法,如果队列为空,该线程就一直阻塞在这,不往下走。
队列中有元素时,就说明有key
过期了,线程继续执行,然后元素出队,根据相应的key
移除缓存。
细节
延时队列中存储的元素需要实现Delayed
接口。
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
getDelay
方法返回剩余的延时时间,如果返回值大于0
,表示还未到入队时间。
同步队列
SynchronousQueue
:同步队列。
最好的解释自然是官方文档:A BlockingQueue in which each insert operation must wait for a corresponding remove operation by another thread, and vice versa.
这是一个阻塞队列,它的特点是在执行插入操作时必须等待另一个线程的移除操作。什么意思呢?
通俗的来说就是买可乐不需要去超市了,我(消费者)直接和厂家(生产者)购买。
所以,生产者和消费者同时存在时,这个交易才能执行,两方达成约定后,生产者生产可乐,卖给消费者。缺少任何一方另一方都会被阻塞,条件满足时会唤醒对方继续执行,这就是所谓的同步。
代码层面讲就是:put
和take
方法都被调用的时候,两者才开始执行,并完成了数据的传递。
所以严格来说,虽然SynchronousQueue
实现了队列接口,但是它的目的却并不是队列,而是将生产者消费者线程配对。
转移队列
LinkedTransferQueue
:链式转移队列。虽然放在了最后,但是查阅相关文档发现,实际的生产环境中,这个队列最常用。
怎么转移的呢?
消费者找队列拿数据,如果没有数据可用,就设置一个标志位,表示我这里期待着一个数据,然后消费者就开始等。
等着等着,直到生产者来了,判断,如果有等着的,就直接把数据给它,实现了数据转移。如果没有呢?就去执行数据入队相关的操作。
总结
点开了阻塞队列的源码,发现线程安全是使用锁实现的。
再看看面试问的东西:乐观锁、悲观锁、自旋锁、偏向锁、公平锁,这都是写啥东西呀?
吾生也有涯,而Java
无涯。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。