一、简介
DelayQueue
是一个支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。也就是说只有在延迟期到时才能够从队列中取元素。
DelayQueue非常有用,可以运用在以下两个应用场景:
- 缓存系统的设计:使用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,就表示有缓存到期了。
- 定时任务调度:使用DelayQueue保存当天要执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如Timer就是使用DelayQueue实现的。
二、继承体系
从继承体系可以看到,DelayQueue实现了BlockingQueue,所以它是一个阻塞队列。
DelayQueue 继承了AbstractQueue,具有了队列的行为。
另外,DelayQueue还组合了一个叫做Delayed的接口,DelayQueue中存储的所有元素必须实现Delayed接口。
那么,Delayed是什么呢?
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
Delayed是一个继承自Comparable的接口,并且定义了一个getDelay()方法,用于表示还有多少时间到期,到期了应返回小于等于0的数值。
三、DelayQueue 数据结构
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
//可重入锁
private final transient ReentrantLock lock = new ReentrantLock();
//存储元素的优先级队列
private final PriorityQueue<E> q = new PriorityQueue<E>();
//获取数据 等待线程标识
private Thread leader = null;
//条件控制,表示是否可以从队列中取数据
private final Condition available = lock.newCondition();
}
- lock:全局独占锁,用于实现线程安全
- q:优先队列,用于存储元素,并按优先级排序
- leader:用于优化内部阻塞通知的线程
- available:用于实现阻塞的Condition对象
其实看到这里,我们应该已经能够了解DelayQueue的大致实现思路了:
以支持优先级的PriorityQueue无界队列作为一个容器,因为元素都必须实现Delayed接口,可以根据元素的过期时间来对元素进行排列,因此,先过期的元素会在队首,每次从队列里取出来都是最先要过期的元素。
1、构造方法
// 默认构造方法,这个简单,什么都没有
public DelayQueue() {}
// 通过集合初始化
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
DelayQueue 内部组合PriorityQueue,对元素的操作都是通过PriorityQueue 来实现的,DelayQueue 的构造方法很简单,对于PriorityQueue 都是使用的默认参数,不能通过DelayQueue 来指定PriorityQueue的初始大小,也不能使用指定的Comparator,元素本身就需要实现Comparable ,因此不需要指定的Comparator。
2、入队
(1)、add(E e)
将指定的元素插入到此队列中,在成功时返回 true
public boolean add(E e) {
return offer(e);
}
(2)、offer(E e)
将指定的元素插入到此队列中,在成功时返回 true,在前面的add 中,内部调用了offer 方法,我们也可以直接调用offer 方法来完成入队操作。
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
// 获取锁
lock.lock();
try {
// 向 PriorityQueue中插入元素
q.offer(e);
// 如果当前元素的队首元素(优先级最高),leader设置为空,唤醒所有等待线程
if (q.peek() == e) {
leader = null;
available.signal();
}
// 无界队列,永远返回true
return true;
} finally {
lock.unlock();
}
}
peek并不一定是当前添加的元素,队头是当前添加元素,说明当前元素e的优先级最小也就即将过期的,这时候激活avaliable变量条件队列里面的一个线程,通知他们队列里面有元素了。
leader是等待获取队列头元素的线程,应用主从式设计减少不必要的等待。如果leader不等于空,表示已经有线程在等待获取队列的头元素。所以,通过await()方法让出当前线程等待信号。如果leader等于空,则把当前线程设置为leader,当一个线程为leader,它会使用awaitNanos()方法让当前线程等待接收信号或等待delay时间。
(3)、offer(E e, long timeout, TimeUnit unit)
public boolean offer(E e, long timeout, TimeUnit unit) {
//调用offer 方法
return offer(e);
}
因为是无界队列,因此不会出现”队满”(超出最大值会抛异常),指定一个等待时间将元素放入队列中并没有意义,队列没有达到最大值那么会入队成功,达到最大值,则失败,不会进行等待。
(4)、put(E e)
将指定的元素插入此队列中,队列达到最大值,则抛oom异常
public void put(E e) {
offer(e);
}
虽然提供入队的接口方式很多,实际都是调用的offer 方法,通过PriorityQueue 来进行入队操作,入队超时方法并没有其超时功能。
3、出队
(1)、poll()
获取并移除此队列的头,如果此队列为空,则返回 null
public E poll() {
final ReentrantLock lock = this.lock;
//获取同步锁
lock.lock();
try {
//获取队头
E first = q.peek();
//如果队头为null 或者 延时还没有到,则返回null
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll(); //元素出队
} finally {
lock.unlock();
}
}
(2)、poll(long timeout, TimeUnit unit)
获取并移除此队列的头部,在指定的等待时间前等待。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
//超时等待时间
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
//可中断的获取锁
lock.lockInterruptibly();
try {
//无限循环
for (;;) {
//获取队头元素
E first = q.peek();
//队头为空,也就是队列为空
if (first == null) {
//达到超时指定时间,返回null
if (nanos <= 0)
return null;
else
// 如果还没有超时,那么再available条件上进行等待nanos时间
nanos = available.awaitNanos(nanos);
} else {
//获取元素延迟时间
long delay = first.getDelay(NANOSECONDS);
//延时到期
if (delay <= 0)
return q.poll(); //返回出队元素
//延时未到期,超时到期,返回null
if (nanos <= 0)
return null;
first = null; // don't retain ref while waiting
// 超时等待时间 < 延迟时间 或者有其它线程再取数据
if (nanos < delay || leader != null)
//在available 条件上进行等待nanos 时间
nanos = available.awaitNanos(nanos);
else {
//超时等待时间 > 延迟时间 并且没有其它线程在等待,那么当前元素成为leader,表示leader 线程最早 正在等待获取元素
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
//等待 延迟时间 超时
long timeLeft = available.awaitNanos(delay);
//还需要继续等待 nanos
nanos -= delay - timeLeft;
} finally {
//清除 leader
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//唤醒阻塞在available 的一个线程,表示可以取数据了
if (leader == null && q.peek() != null)
available.signal();
//释放锁
lock.unlock();
}
}
来梳理梳理这里的逻辑:
1、如果队列为空,如果超时时间未到,则进行等待,否则返回null
2、队列不空,取出队头元素,如果延迟时间到,则返回元素,否则 如果超时 时间到 返回null
3、超时时间未到,并且超时时间< 延迟时间或者有线程正在获取元素,那么进行等待
4、超时时间> 延迟时间,那么肯定可以取到元素,设置leader为当前线程,等待延迟时间到期。
这里需要注意的时Condition 条件在阻塞时会释放锁,在被唤醒时会再次获取锁,获取成功才会返回。
当进行超时等待时,阻塞在Condition 上后会释放锁,一旦释放了锁,那么其它线程就有可能参与竞争,某一个线程就可能会成为leader(参与竞争的时间早,并且能在等待时间内能获取到队头元素那么就可能成为leader)
leader是用来减少不必要的竞争,如果leader不为空说明已经有线程在取了,设置当前线程等待即可。(leader 就是一个信号,告诉其它线程:你们不要再去获取元素了,它们延迟时间还没到期,我都还没有取到数据呢,你们要取数据,等我取了再说)
下面用流程图来展示这一过程:
(3)、take()
获取并移除此队列的头部,在元素变得可用之前一直等待
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
// 队首元素
E first = q.peek();
// 如果队首元素为空,说明队列中还没有元素,直接阻塞等待
if (first == null)
available.await();
else {
// 队首元素的到期时间
long delay = first.getDelay(NANOSECONDS);
// 如果小于0说明已到期,直接调用poll()方法弹出队首元素
if (delay <= 0)
return q.poll();
// 如果delay大于0 ,则下面要阻塞了
// 将first置为空方便gc,因为有可能其它元素弹出了这个元素
// 这里还持有着引用不会被清理
first = null; // don't retain ref while waiting
// 如果前面有其它线程在等待,直接进入等待
if (leader != null)
available.await();
else {
// 如果leader为null,把当前线程赋值给它
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 等待delay时间后自动醒过来
// 醒过来后把leader置空并重新进入循环判断队首元素是否到期
// 这里即使醒过来后也不一定能获取到元素
// 因为有可能其它线程先一步获取了锁并弹出了队首元素
// 条件锁的唤醒分成两步,先从Condition的队列里出队
// 再入队到AQS的队列中,当其它线程调用LockSupport.unpark(t)的时候才会真正唤醒
// 关于AQS我们后面会讲的^^
available.awaitNanos(delay);
} finally {
// 如果leader还是当前线程就把它置为空,让其它线程有机会获取元素
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 成功出队后,如果leader为空且队首还有元素,就唤醒下一个等待的线程
if (leader == null && q.peek() != null)
// signal()只是把等待的线程放到AQS的队列里面,并不是真正的唤醒
available.signal();
// 解锁,这才是真正的唤醒
lock.unlock();
}
}
该方法就是相当于在前面的超时等待中,把超时时间设置为无限大,那么这样只要队列中有元素,要是元素延迟时间要求,那么就可以取出元素,否则就直接等待元素延迟时间到期,再取出元素,最先参与等待的线程会成为leader。
(4)、peek()
调用此方法,可以返回队头元素,但是元素并不出队。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//返回队列头部元素,元素不出队
return q.peek();
} finally {
lock.unlock();
}
}
四、案例Demo
源码虽然看了,但我们还是得动手来实践下,下面来看个案例Demo:
package com.concurrent;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @author riemann
* @date 2019/09/01 1:38
*/
public class DelayQueueTest {
public static void main(String[] args) {
DelayQueue<Message> queue = new DelayQueue<>();
long now = System.currentTimeMillis();
// 启动一个线程从队列中取元素
new Thread(()->{
while (true) {
try {
// 将依次打印1000,2000,5000,7000,8000
System.out.println(queue.take().deadline - now);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 添加5个元素到队列中
queue.add(new Message(now + 5000));
queue.add(new Message(now + 8000));
queue.add(new Message(now + 2000));
queue.add(new Message(now + 1000));
queue.add(new Message(now + 7000));
}
}
class Message implements Delayed {
long deadline;
public Message(long deadline) {
this.deadline = deadline;
}
@Override
public long getDelay(TimeUnit unit) {
return deadline - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
return (int) (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public String toString() {
return "Message{" +
"deadline=" + deadline +
'}';
}
}
输出结果:
1000
2000
5000
7000
8000
由结果可以看出:越早到期的元素越先出队。
五、总结
- DelayQueue 内部通过组合PriorityQueue 来实现存储和维护元素顺序的。
- DelayQueue 存储元素必须实现Delayed 接口,通过实现Delayed 接口,可以获取到元素延迟时间,以及可以比较元素大小(Delayed 继承Comparable)
- DelayQueue 通过一个可重入锁来控制元素的入队出队行为
- DelayQueue 中leader 标识用于减少线程的竞争,表示当前有其它线程正在获取队头元素。
- PriorityQueue 只是负责存储数据以及维护元素的顺序,对于延迟时间取数据则是在DelayQueue 中进行判断控制的。
- DelayQueue 没有实现序列化接口