前言
有时候,我们有一些任务需要“稍后”来做,比如一些连接需要空闲一段时间后再关闭,session需要空闲一段时间后自动退出。这个时候就需要一些可以延迟执行任务的工具。DelayQueue
(延迟队列)就是一个可以实现类似功能的工具。
由于本篇会涉及到优先队列PriorityQueue
,所以预先阅读 深度解析优先级队列PriorityQueue 很有必要。
DelayQueue
DelayQueue
(延迟队列)的标准实现出现在JDK1.5中的J.U.C包中,作为一个工具类,用来管理一些需要延迟处理的任务。先来看下它的类体系结构:
需要注意的是DelayQueue
实现了BlockingQueue
接口,意味着它也是阻塞队列,但是类名称中没有包含Blocking
示例演示
因为DelayQueue
比一般队列使用起来稍微复杂一定,所以先看一个实例:
/**
* @author sicimike
*/
public class DelayQueueDemo {
public static void main(String[] args) {
final long currentTime = System.currentTimeMillis();
DelayQueue<DelayObject> queue = new DelayQueue();
queue.offer(new DelayObject("Sic-001", currentTime + 6000L));
queue.offer(new DelayObject("Sic-002", currentTime + 3000L));
queue.offer(new DelayObject("Sic-003", currentTime + 8000L));
System.out.println(System.currentTimeMillis() + " -> 入队完成" );
while (!queue.isEmpty()) {
try {
DelayObject delayObject = queue.take();
System.out.println(System.currentTimeMillis() + " -> " + delayObject);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
其中队列内部元素DelayObject
定义如下:
/**
* 延迟队列中的元素
* 必须实现Delayed接口
* @author sicimike
*/
class DelayObject implements Delayed {
// 过期时间
private Long destroyTime;
private String id;
@Override
public long getDelay(TimeUnit unit) {
// 返回元素剩余的时间
return unit.convert(destroyTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
// 剩余时间少的,排在队列前面
return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
public DelayObject() {
}
public DelayObject(String id, Long destroyTime) {
this.id = id;
this.destroyTime = destroyTime;
}
public Long getDestroyTime() {
return destroyTime;
}
public void setDestroyTime(Long destroyTime) {
this.destroyTime = destroyTime;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String toString() {
return "DelayObject {" +
"destroyTime=" + destroyTime +
", id='" + id + '\'' +
'}';
}
}
执行结果:
1576763926966 -> 入队完成
1576763929959 -> DelayObject {destroyTime=1576763929959, id='Sic-002'}
1576763932959 -> DelayObject {destroyTime=1576763932959, id='Sic-001'}
1576763934961 -> DelayObject {destroyTime=1576763934959, id='Sic-003'}
从执行结果可以看出,三个元素入队完成后,大概延迟了3秒,出队了第一个元素;继续延迟3秒,出队第二个元素;继续延迟2秒,出队第三个元素。
元素的出队顺序与放入的顺序无关,而与元素延迟的时间有关,按照升序排列出队,顺序由DelayObject
中的compareTo
方法决定。
放入DelayQueue
中的元素必须实现Delayed
接口,实现接口中的getDelay
方法和compareTo
方法。getDelay
获取的是元素剩余的时间,所以不同的时刻调用该方法,应该得到不同的值,否则元素永远不会过期。compareTo
方法用于给元素排序,排在前面的先出队。
这就是延迟队列DelayQueue
的基本使用。
内部结构
大致了解DelayQueue
的使用后,再来看下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;
/**
* Condition signalled when a newer element becomes available
* at the head of the queue or a new thread may need to
* become leader.
*/
private final Condition available = lock.newCondition();
}
根据定义可以看出底层通过组合的方式持有优先队列PriorityQueue
的对象,也就是说延迟队列DelayQueue
底层是通过优先队列PriorityQueue
来实现的。
核心方法
需要重点关注的核心操作只有三个:入队、出队、查看队首元素。
offer(E e)方法
offer(E e)
方法用于往DelayQueue
中放入元素。由于DelayQueue
是无界队列(底层PriorityQueue
可以自动扩容),所以往DelayQueue
中放入元素是非阻塞的。put
、add
均是调用offer
方法,其实现如下:
public void put(E e) {
offer(e);
}
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
// 真正完成入队操作的方法
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 入队(直接调用PriorityQueue的入队方法)
q.offer(e);
if (q.peek() == e) {
// 放入的元素恰好是下一个要出队的元素
// 唤醒因调用take方法而被阻塞的线程
// leader设置为null是为了使当前队首元素快速进入设置了时间的阻塞状态(结合take方法)
leader = null;
available.signal();
}
return true;
} finally {
// 解锁
lock.unlock();
}
}
take()方法
take()
方法以阻塞的方式从DelayQueue
中取出一个元素。
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);
if (delay <= 0)
// 时间到了,出队
return q.poll();
// 等待的时候不需要持有队首元素引用
first = null; // don't retain ref while waiting
if (leader != null)
// leader不为null,表示有线程已经设置了阻塞时间,当前线程直接被阻塞
available.await();
else {
// 没有线程设置过阻塞时间(或者阻塞时间已经到了)
Thread thisThread = Thread.currentThread();
// 当前线程设置成leader线程
leader = thisThread;
try {
// 因为队首元素还有delay时间才能出队,所以休眠delay时间
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
// 休眠时间到了,释放leader
// 下一次循环出队
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
// 前一个元素已经出队,并且队列中还有元素
// 唤醒其他被阻塞的线程
available.signal();
// 解锁
lock.unlock();
}
}
通过take
方法可以看出DelayQueue
使用ReentrantLock
+ Condition
来实现线程的阻塞、唤醒。再加上循环,如果队首元素不出队,线程会一直被阻塞。
这里的leader使用的是Leader/Follower
模型,队首元素尚未出队时,leader不为null。其余的线程知道leader不为null之后就被无限阻塞;队首元素出队后,leader为null,通知其余的线程来争抢。
总结
DelayQueue
实现了BlockingQueue
接口,是阻塞队列。底层利用了优先队列PriorityQueue
的自动扩容、排序等机制。再利用ReentrantLock
+ Condition
机制实现线程安全,以及线程休眠、唤醒。