DelayQueue講解
DelayQueue 是一個帶延時功能的阻塞隊列,可以通過它輕鬆的實現定時任務、延時任務,比如重試、異步提醒、定時通知等等。
那DelayQueue爲何有這樣的能力?他是如何做的呢?
JDK給他的定位是什麼
在看一個類時,先看作者給他的定位是什麼,怎麼看?通過類定義和類關係圖來了解。
類定義
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E>
類關係圖
看他直接關係,發現它是一個Queue,有BlockingQueue的能力,但有個限制,往它裏面放的元素必須實現Delayed接口。
作者的實現思路
瞭解一個東西要先從表面看,一點一點深入,而不是直接來看,第一步怎麼看?先看它有哪些成員變量。
// 鎖:控制線程安全
private final transient ReentrantLock lock = new ReentrantLock();
// 內部容器,PriorityQueue是一個二分小頂堆
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 等待一定時間的線程,除了leader線程外,其他線程都一直休息,直到被通知喚醒
private Thread leader;
// 從隊列裏取內容失敗是需要在這個變量裏等待
private final Condition available = lock.newCondition();
可以看到只有四個成員變量,每個變量的作用已註釋,大概可以猜想出 DelayQueue 是在 PriorityQueue(優先隊列)的基礎上封裝了一層(1.要求隊列內元素必須實現Delayed能力;2.控制線程安全和阻塞等待)
我們思路已經有了,首先得有排序的能力,保證延遲少的在隊列首部,延遲大的在後面,這樣才能保證每次取到的是最先要過期的元素,由於PriorityQueue已經實現排序的能力,因此我們直接使用這個類即可。
其次,我們需要保證線程安全,因爲 PriorityQueue 是線程不安全的,我們便引入了Lock,只要在調用方法前進行加鎖,調用後解鎖即可。
最重要的是控制線程的等待和喚醒,而這個能力Condition已經實現,引入它即可。
最後,我們定義一個Thread leader
,只需保證一個線程在等即可(等待時間爲第一個元素的過期時間),即所有執行take或者poll方法的線程中僅僅一個來等指定時間,其他線程由這個 leader 線程執行available.single()
來喚醒。
具體實現
思路我們已經有了,接下來看具體怎麼實現的。
小白看源碼時候特別容易翻車,why?很可能是因爲太過於糾結細節,我們第一遍的時候沒必要糾結每一行代碼什麼意思,跟着我思路走:先看常用的,如構造函數,put、take,其他的先忽略。
構造函數
沒什麼特別的,和其他容器類似,提供設置初始大小,通過已有容器創建等。
put
往裏放時候最常用的方法。
public void put(E e) {
offer(e);
}
// 發現它其實調用了 offer,來看 offer
public boolean offer(E e) {
// 先加鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 放進優先隊列
q.offer(e);
// 因爲優先隊列 q 是有順序的,因此放進去立馬拿,不一定是放的元素
if (q.peek() == e) {
// 如果放進去取出來還是e,只有兩種情況,見下文解釋。
leader = null;
available.signal();
}
return true;
} finally {
// 釋放鎖
lock.unlock();
}
}
可以看到由於內部使用的其實是 PriorityQueue ,而PriorityQueue 是沒有大小限制的(Integer的最大值),因此加元素的方法肯定不會阻塞,一定返回true,不信可以看帶阻塞時間的offer,可以看到,等待時間參數被忽略了。
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
回到正題,放元素的方法我們只需要關注一個點,發現它在放入之後又看了一下隊首的元素,有個if判斷兩者是否相等,我們知道 PriorityQueue 是自動排序的,因此放進去立馬拿,不一定是剛剛放的元素。
if 條件成立有以下兩種可能性:
- 放之前隊列裏是空的
- 剛放入的 e 的過期時間是當前所有元素中最小的
第一情況下,就需要喚醒等待者,別等了,嘗試拿走吧。
而第二種情況,剛放了又是最小的,說明不需要等待原來預計的等待時間了,將提前執行,而修改等待時間的代碼實現在take中,本線程只需要告訴等着的線程一聲,而不是幫它做了。
take
public E take() throws InterruptedException {
// 加鎖
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 死循環:不拿到元素,絕不罷休
for (;;) {
// 1. 先看看隊列裏有元素嗎,沒有就去歇着,等着別人來叫我我再幹活
E first = q.peek();
if (first == null)
available.await();
else {
// 2. 發現隊列裏有元素
long delay = first.getDelay(NANOSECONDS);
// 2.1 看看元素是不是過期了,過期了我就拿走了
if (delay <= 0L)
return q.poll();
first = null; // don't retain ref while waiting 既然沒過期,我還不能拿,那我就不要它了。
// 2.2 看看是否有其他線程來管啥時候能拿不,有的話,我就直接一直休息就行了,直到有人通知我
if (leader != null)
available.await();
else {
// 2.3 既然沒有線程來負責這件事,那我就當leader,負責這件事
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 我不能和2.2那樣的線程不負責,當前第一個任務過期了,我再來幹活,休息一定的時間,而不是一直休息。
available.awaitNanos(delay);
} finally {
// 我都醒了,這次leader我就先讓讓,再次循環嘗試拿咯
if (leader == thisThread)
leader = null;
}
}
}
}
// 釋放鎖
} finally {
// 如果沒有lead了,而且隊列裏還有要執行的,那我就通知一下其他等待的線程,別歇着了,來領任務了~
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
註釋的比較詳細了已經,主要分以下幾種情況
- 隊列中沒有任何元素
- 一直等着,等別人叫醒我
- 隊列裏有元素,而且過期了
- 既然可以拿走,我就拿走咯,return 返回~
- 隊列裏有元素,但是還沒過期
- leader已經有人當了
- 一直等着,等別人叫醒我
- leader沒人當
- 我當leader
- leader已經有人當了
這裏核心點主要是,等待者有兩種角色,一種是一直等着,一種是leader,而leader有什麼特別的呢?
DelayQueue核心
DelayQueue核心即劃分了兩種等待者的角色
- 普通等待者 線程
- leader 線程
普通的比較簡單,一句話就能概括,一直等着,沒人叫我我不醒。
leader 角色相對來說負責一些,隊列中第一個元素什麼時候過期,我啥時候醒,當然也可能提前醒來:新加元素的過期時間比之前預計的醒來時間還早,這時候重新進入循環,嘗試取元素。
用法示例
DelayQueue 思想和原理我們都懂了,那怎麼用呢?
留白等催更。