前言
有時候,我們有一些任務需要“稍後”來做,比如一些連接需要空閒一段時間後再關閉,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
機制實現線程安全,以及線程休眠、喚醒。