本文原發表於簡書DelayQueue之原始碼分析。
本文將會對DelayQueue做一個簡單的介紹,並提供部分原始碼的分析。
DelayQueue的特性基本上由BlockingQueue、PriorityQueue和Delayed的特性來決定的。
簡而言之,DelayQueue是通過Delayed,使得不同元素之間能按照剩餘的延遲時間進行排序,然後通過PriorityQueue,使得超時的元素能最先被處理,然後利用BlockingQueue,將元素處理的操作阻塞住。
基本定義如下:
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();
}
複製程式碼
ReentrantLock lock = new ReentrantLock(); ReentrantLock是一個可重入的互斥鎖,將由最近成功獲得鎖,並且還沒有釋放該鎖的執行緒所擁有,當鎖被其他執行緒獲得時,呼叫lock的執行緒將無法獲得鎖。 在DelayQueue中,只有一個互斥鎖lock。
PriorityQueue q = new PriorityQueue(); PriorityQueue是一個優先順序佇列,每次從佇列中取出的是具有最高優先權的元素。 在DelayQueue中,因為E繼承於Delayed,所以q表示一個按照delayTime排序的優先順序佇列,用於存放需要延遲執行的元素。
Thread leader = null; 這裡的leader設計出來是為了minimize unnecessary timed waiting(減少不必要的等待時間),如何實現的方案會在詳細解讀中解釋。 在DelayQueue中leader表示一個等待從佇列中獲取訊息的執行緒。
Condition available = lock.newCondition(); Condition是lock物件的條件變數,只能和鎖lock配合使用,用於控制併發程式訪問競爭資源的安全。 一個鎖lock可以有多個條件變數condition,每個條件上可以有多個執行緒等待,通過呼叫await()方法,可以讓執行緒在該條件下等待。當呼叫signalAll()方法,又可以喚醒該條件下的等待的執行緒。 在DelayQueue中lock物件只有一個條件變數available。
以下是DelayQueue的主要方法:
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
複製程式碼
1、執行lock.lock(),獲取鎖。
2、把元素e新增到優先佇列q(下稱佇列q)中。
3、判斷佇列q的隊首元素是否為e。
4、如果e是隊首元素的話,即元素e是最近可被執行的元素,意味著延遲佇列的執行順序將被變更。 執行leader = null,否則在執行take時,所有執行緒就會在if(leader!=null)的判斷下進入等待。 執行available.signal(),喚醒其他等待中的執行緒,重新去迴圈執行take中的操作1-8。 如果不執行signal,那麼在take方法中,只有執行awaitNanos(delay)的執行緒在等待delay指定的時間後自動喚醒,其他執行await的執行緒將一直被掛起。 如果沒有新的執行緒去執行take方法,那麼等待執行awaitNanos(delay)的執行緒自動喚醒時,此時等待時間將超過元素e的delayTime,這不符合預期。 即便有新的執行緒去執行take方法,那之前掛起的執行緒也將一直在等待,效率很低。
5、在finally塊中執行lock.unlock()。 需要注意的是,鎖必須在 finally 塊中釋放。否則,如果程式碼丟擲異常,那麼鎖就有可能永遠得不到釋放。如果沒有釋放鎖,那麼就會產生死鎖的問題。
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)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
複製程式碼
1、執行lock.lockInterruptibly(),獲取鎖。 lockInterruptibly和lock的區別在於 lock 在鎖被其他執行緒佔有,當前執行緒等待鎖期間(下稱等待鎖期間),只考慮獲取鎖。只有在獲取鎖成功後,才會去響應中斷。 而lockInterruptibly 在等待鎖期間,會優先考慮響應中斷,而不是響應鎖的獲取。如果當前執行緒被打斷(interrupt)則該方法丟擲InterruptedException。該方法提供了一種解除死鎖的途徑。
2、E first = q.peek(),獲取佇列q的隊首元素first(下稱first)。
3、如果first為空,則執行avaliable.await()讓執行緒進入等待。實際上就是釋放鎖,然後掛起執行緒,等待被喚醒,此時其他執行緒可以獲得鎖了。 await()和awaitNanos(nanosTimeout)區別在於 執行awaitNanos(nanosTimeout)的執行緒比執行await()的執行緒多一個喚醒條件,超過等待nanosTimeout指定的時間,執行緒將自動喚醒。執行緒喚醒時,保證該執行緒是持有鎖的。
4、如果first不為空,則執行first.getDelay(NANOSECONDS)獲取first的剩餘延遲時間delayTime(下稱delayTime)
5、如果first的delayTime<=0,表明該元素已經達到之前設定的延遲時間了,則呼叫return q.poll(),將first從佇列q中的移除並且返回該元素first.
6、如果first的delayTime>0,則將first指向null,釋放first的引用,避免記憶體洩露.
7、如果執行緒leader(下稱leader)不為空的話,則執行avaliable.await()讓執行緒進入等待。leader不為空的話,表明已經有其他執行緒在獲取優先佇列q的隊首元素了(下稱獲取隊首元素),此時只需要執行avaliable.await()讓當前執行緒進入等待即可。
8、如果leader為空,則執行Thread thisThread = Thread.currentThread();leader = thisThread;將leader指向當前執行緒,然後執行available.awaitNanos(delay);讓執行緒最長等待delayTime的時間。最後在finally塊中,如果leader依然指向前文獲取的當前執行緒thisThread,那麼將leader指向null,釋放leader引用。 這裡leader為空,表明尚未有其他執行緒在獲取隊首元素,此時設定leader物件,指向當前執行緒(下稱currentThread)。因為currentThread執行了available.awaitNanos(delay)釋放了鎖,所以其他執行緒(下稱otherThread)在呼叫take方法時能獲取鎖,但是因為leader非空,所以otherThread都會進入7的那步,直接進入等待,而不需要像currentThread那樣執行8的一系列操作,達到設計leader執行緒的初衷。
9、迴圈執行以上1-8步,直到first非空且first的delayTime<=0,跳出迴圈。
10、跳出迴圈後,進入finally塊。
11、如果leader為空且佇列q的隊首元素非null(q佇列中移除了上文的first元素後還有其他元素),此時執行available.signal(),呼叫signal喚醒其他等待中的執行緒。
12、執行lock.unlock(),執行解鎖操作。
ok,原始碼分析就先講到這裡了,下一期我準備講一下如何將DelayQueue封裝成可用的元件,讓使用者呼叫起來更加方便。