一、簡介
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 沒有實現序列化接口