DelayQueue阻塞隊列系列文章
DelayQueue阻塞隊列第一章:代碼示例
DelayQueue阻塞隊列第二章:源碼解析
介紹
DelayQueue是java併發包中提供的延遲阻塞隊列,業務場景一般是下單後多長時間過期,定時執行程序等
1-DelayQueue的組成結構
/**
* DelayQueue隊列繼承了AbstractQueue,並且實現BlockingQueue的方法
*/
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
//使用ReentrantLock進行線程的同步
private final transient ReentrantLock lock = new ReentrantLock();
//使用優先級隊列PriorityQueue作爲存放數據的隊列
private final PriorityQueue<E> q = new PriorityQueue<E>();
//使用leader/follower模式來避免多線程性能的消耗
private Thread leader = null;
//使用Condition等待隊列來保存請求的線程(l/f模式)
private final Condition available = lock.newCondition();
DelayQueue中的元素需要實現Delayed接口,重寫getDelay()和compareTo()方法,其中getDelay()方法是爲了獲取隊列元素延遲剩餘時間,compareTo()方法是爲了對隊列中的元素進行一個排序,使符合條件的元素排在隊列的最前面
DelayQueue內部的實現基本就是依靠重寫BlockingQueue方法,使用ReentrantLock進行同步操作,使用PriorityQueue存放隊列元素,Condition存放訪問線程
DelayQueue內部採用了leader/follower設計模式,旨在減小多線程的消耗,本文不詳細介紹
2源碼實現細節
offer方法:將元素加入到延遲隊列中去
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//元素加入優先級隊列
q.offer(e);
//如果新加入的元素e就是隊列的頭元素,將leader置爲null切喚醒等待線程
if (q.peek() == e) { //Q1此處爲何要獲取隊列頭節點元素並與新加入元素進行比較
leader = null; //Q2爲何要將leader線程置爲null且喚起等待隊列
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
offer方法比較簡單,只針對以上兩處做詳細說明
Q1和Q2兩個操作都是爲了解決一個問題,就是leader對應隊列首節點元素的問題,因爲元素是不斷在加入的,比如,leader對應需要取出的首節點是A,此時A雖然是首節點元素,但是還沒有到達延遲時間,所以leader還在等待A,他們的關係是對應的(對應關係的邏輯參考take()源碼),那麼此時加入了元素B,這時候元素B排在了隊首,那麼此時需要處理元素B的就不再是當前的leader了,所以我們需要將leader置空,重新選取新的leader來處理這個B,至於之前的leader線程,在take源碼中,在調用available.awaitNanos(delay)後,當時間到了會重新獲取鎖然後執行操作
所以我們要首先判斷加入的新元素是否是首節點,以便確定對應線程的處理關係
絕大多數的文章對源碼中爲什麼進行if (q.peek() == e)和leader = null的操作的原因隻字不提,我覺得還是有必要寫下的,我對於此處原因的理解可能也存在偏差,希望各位不吝賜教
take方法:取出元素並處理元素事件
/**
* 首先獲取優先隊列的首個元素,如果爲空則調用線程沉睡。
* 如果優先級隊列不爲空,查看當前首元素是否到達過期時間,到達過期時間了就獲取並移除隊列
* 如果沒有到達過期時間,將first變量置爲null(防止內存泄漏),如果leader線程不爲空則進入等待隊列
* 如果leader爲空,則當前線程爲leader,並限時進入等待隊列中進行等待
* 如果leader爲空,隊列中還有元素存在,則喚醒所有等待的follower線程
* 繼續循環,直到獲取延時隊列中的元素
*/
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) //如果延時時間小於或是等於0,則移出隊列
return q.poll();
first = null; // don't retain ref while waiting防止內存泄漏
if (leader != null) //說明leader線程正在工作,當前線程就進入等待隊列中
available.await();//當前線程轉變爲follower線程
else { //如果首節點不爲空,延時時間還沒到,沒有相應的處理線程
Thread thisThread = Thread.currentThread(); //獲取當前線程
leader = thisThread; //當前線程設置爲首線程
try {
available.awaitNanos(delay); //限時進入等待隊列中處理延時時間最小的元素,並釋放鎖
} finally {
if (leader == thisThread)
leader = null; //執行事件之後,將leader線程置爲null讓給其他線程
}
}
}
}
} finally {
if (leader == null && q.peek() != null) //如果leader線程爲null,優先級隊列中還有元素,則喚醒通知隊列中的線程
available.signal();
lock.unlock();
}
}
take方法是DelayQueue的核心方法,獲取延遲隊列中的元素,檢索並移除這個隊列的頭部,等待直到這個隊列的過期元素可用
關於源碼的疑惑,不將first=null爲什麼會導致內存泄露?
核心點在於leader調用await方法時會釋放鎖,比如,當線程A獲取了first,然後將當前線程設爲leader線程,接着進入await方法,釋放鎖,這時線程B也獲取了first,因爲leader != null,所以進入阻塞隊列,這時線程A從等待隊列中返回,獲取對象釋放first,但由於線程B中依然有first的引用,所以gc無法對first進行回收,導致內存的泄露
在DelayQueue還有很多值得研究的源碼和問題,我在日後也會慢慢的加上來,第一次寫先寫這麼多吧,不足之處希望可以共同討論進步!
- 技術理解不到或有錯誤請直(bu)接(yao)指(ma)出(wo)
- 寫作不易!
- 更好閱讀體驗請移步簡書