Java並發包源碼學習系列:阻塞隊列實現之DelayQueue源碼解析
系列傳送門:
- Java並發包源碼學習系列:AbstractQueuedSynchronizer
- Java並發包源碼學習系列:CLH同步隊列及同步資源獲取與釋放
- Java並發包源碼學習系列:AQS共享式與獨佔式獲取與釋放資源的區別
- Java並發包源碼學習系列:ReentrantLock可重入獨佔鎖詳解
- Java並發包源碼學習系列:ReentrantReadWriteLock讀寫鎖解析
- Java並發包源碼學習系列:詳解Condition條件隊列、signal和await
- Java並發包源碼學習系列:掛起與喚醒執行緒LockSupport工具類
- Java並發包源碼學習系列:JDK1.8的ConcurrentHashMap源碼解析
- Java並發包源碼學習系列:阻塞隊列BlockingQueue及實現原理分析
- Java並發包源碼學習系列:阻塞隊列實現之ArrayBlockingQueue源碼解析
- Java並發包源碼學習系列:阻塞隊列實現之LinkedBlockingQueue源碼解析
- Java並發包源碼學習系列:阻塞隊列實現之PriorityBlockingQueue源碼解析
DelayQueue概述
DelayQueue是一個支援延時獲取元素的無界阻塞隊列,使用PriorityQueue來存儲元素。
隊中的元素必須實現Delayed介面【Delay介面又繼承了Comparable,需要實現compareTo方法】,每個元素都需要指明過期時間,通過getDelay(unit)獲取元素剩餘時間【剩餘時間 = 到期時間 - 當前時間】,每次向優先隊列中添加元素時根據compareTo方法作為排序規則。
當從隊列獲取元素時,只有過期的元素才會出隊列。
使用場景: 快取系統設計、定時任務調度等。
類圖及重要欄位

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>();
/**
* 基於Leader-Follower模式的變體,用於盡量減少不必要的執行緒等待
*/
private Thread leader = null;
/**
* 與lock對應的條件變數
*/
private final Condition available = lock.newCondition();
}
- 使用ReentrantLock獨佔鎖實現執行緒同步,使用Condition實現等待通知機制。
- 基於Leader-Follower模式的變體,減少不必要的執行緒等待。
- 內部使用PriorityQueue優先順序隊列存儲元素,且隊列中元素必須實現Delayed介面。
Delayed介面
隊中的元素必須實現Delayed介面【Delay介面又繼承了Comparable,需要實現compareTo方法】,每個元素都需要指明過期時間,通過getDelay(unit)獲取元素剩餘時間【剩餘時間 = 到期時間 - 當前時間】。
每次向優先隊列中添加元素時根據compareTo方法作為排序規則,當然我們約定一下,默認q.peek()出來的就是最先過期的元素。
public interface Delayed extends Comparable<Delayed> {
// 返回剩餘時間
long getDelay(TimeUnit unit);
}
public interface Comparable<T> {
// 定義比較方法
public int compareTo(T o);
}
Delayed元素案例
學習了Delayed介面之後,我們看一個實際的案例,加深印象,源於:《Java並發編程之美》。
static class DelayedElement implements Delayed {
private final long delayTime; // 延遲時間
private final long expire; // 到期時間
private final String taskName; // 任務名稱
public DelayedElement (long delayTime, String taskName) {
this.delayTime = delayTime;
this.taskName = taskName;
expire = now() + delayTime;
}
final long now () {
return System.currentTimeMillis();
}
// 剩餘時間 = 到期時間 - 當前時間
@Override
public long getDelay (TimeUnit unit) {
return unit.convert(expire - now(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo (Delayed o) {
return (int) (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public String toString () {
final StringBuilder res = new StringBuilder("DelayedElement [ ");
res.append("delay = ").append(delayTime);
res.append(", expire = ").append(expire);
res.append(", taskName = '").append(taskName).append('\'');
res.append(" ] ");
return res.toString();
}
}
public static void main (String[] args) {
// 創建delayQueue隊列
DelayQueue<DelayedElement> delayQueue = new DelayQueue<>();
// 創建延遲任務
Random random = new Random();
for (int i = 0; i < 10; i++) {
DelayedElement element = new DelayedElement(random.nextInt(500), "task: " + i);
delayQueue.offer(element);
}
// 依次取出任務並列印
DelayedElement ele = null;
try {
for (; ; ) {
while ((ele = delayQueue.take()) != null) {
System.out.println(ele);
}
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
// 列印結果
DelayedElement [ delay = 2, expire = 1611995426061, taskName = 'task: 4' ]
DelayedElement [ delay = 52, expire = 1611995426111, taskName = 'task: 2' ]
DelayedElement [ delay = 80, expire = 1611995426139, taskName = 'task: 5' ]
DelayedElement [ delay = 132, expire = 1611995426191, taskName = 'task: 0' ]
DelayedElement [ delay = 174, expire = 1611995426233, taskName = 'task: 9' ]
DelayedElement [ delay = 175, expire = 1611995426234, taskName = 'task: 7' ]
DelayedElement [ delay = 326, expire = 1611995426385, taskName = 'task: 3' ]
DelayedElement [ delay = 447, expire = 1611995426506, taskName = 'task: 8' ]
DelayedElement [ delay = 452, expire = 1611995426511, taskName = 'task: 1' ]
DelayedElement [ delay = 486, expire = 1611995426545, taskName = 'task: 6' ]
- 實現了compareTo方法,定義比較規則為越早過期的排在隊頭。
- 實現了getDelay方法,計算公式為:剩餘時間 = 到期時間 - 當前時間。
構造器
DelayQueue構造器相比於前幾個,就顯得非常easy了。
public DelayQueue() {}
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
void put(E e)
因為DelayQueue是無界隊列,不會因為邊界問題產生阻塞,因此put操作和offer操作是一樣的。
public void put(E e) {
offer(e);
}
public boolean offer(E e) {
// 獲取獨佔鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 加入優先隊列里
q.offer(e);
// 判斷堆頂元素是不是剛剛插入的元素
// 如果判斷為true,說明當前這個元素是將最先過期
if (q.peek() == e) {
// 重置leader執行緒為null
leader = null;
// 激活available變數條件隊列中的一個執行緒
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
E take()
take方法將會獲取並移除隊列裡面延遲時間過期的元素 ,如果隊列裡面沒有過期元素則陷入等待。
public E take() throws InterruptedException {
// 獲取獨佔鎖
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
// 瞅一瞅誰最快過期
E first = q.peek();
// 隊列為空,則將當前執行緒置入available的條件隊列中,直到裡面有元素
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
// leader不為null表示其他執行緒也在執行take
// 則將當前執行緒置入available的條件隊列中
if (leader != null)
available.await();
else {
// 如果leader為null,則選擇當前執行緒作為leader執行緒
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 等待delay時間,時間到之後,會出條件隊列,繼續競爭鎖
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
first = null 有什麼用
如果不設置first = null,將會引起記憶體泄露。
- 執行緒A到達,隊首元素沒有到期,設置leader = 執行緒A,並且執行
available.awaitNanos(delay);等待元素過期。- 這時執行緒B來了,因為leader != null,則會
available.await();阻塞,執行緒C、D、E同理。- 執行緒A阻塞完畢了,再次循環,獲取列首元素成功,出列。
這個時候列首元素應該會被回收掉,但是問題是它還被執行緒B、執行緒C持有著,所以不會回收,如果執行緒增多,且隊首元素無限期的不能回收,就會造成記憶體泄漏。
總結
DelayQueue是一個支援延時獲取元素的無界阻塞隊列,使用PriorityQueue來存儲元素。
隊中的元素必須實現Delayed介面【Delay介面又繼承了Comparable,需要實現compareTo方法】,每個元素都需要指明過期時間,通過getDelay(unit)獲取元素剩餘時間【剩餘時間 = 到期時間 - 當前時間】,每次向優先隊列中添加元素時根據compareTo方法作為排序規則。
基於Leader-Follower模式使用leader變數,減少不必要的執行緒等待。
DelayQueue是無界隊列,因此插入操作是非阻塞的。但是take操作從隊列獲取元素時,是阻塞的,阻塞規則為:
- 當一個執行緒調用隊列的take方法,如果隊列為空,則將會調用
available.await()陷入阻塞。 - 如果隊列不為空,則查看隊列的隊首元素是否過期,根據getDelay的返回值是否小於0判斷,如果過期則返回該元素。
- 如果隊首元素未過期,則判斷當前執行緒是否為leader執行緒,如果不是,表明有其他執行緒在執行take操作,就調用
available.await()陷入阻塞。 - 如果沒有其他執行緒在執行take,就將當前執行緒設置為leader,並等待隊首元素過期,
available.awaitNanos(delay)。 - leader執行緒退出take之後,將會調用
available.signal()喚醒一個follower執行緒,接著回到開始那步。
參考閱讀
-
《Java並發編程的藝術》
-
《Java並發編程之美》

