DelayQueue是一個支持延時獲取元素的***阻塞隊列。並且隊列中的元素必須實現Delayed接口。在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中獲取到元素。DelayQueue的應用範圍非常廣闊,如可以用它來保存緩存中元素的有效期,也可用它來實現定時任務。
Delayed接口
在分析DelayQueue源碼之前,我們先來看看Delayd接口,其源碼定義如下:
public interface Delayed extends Comparable < Delayed > {
/**
* 指定返回對象的延時時間
* @param unit [時間單位]
* @return [延時的剩餘,0或者-1表示延時已經過期]
*/
long getDelay(TimeUnit unit);
}
我們看到,Delayed接口繼承了Comparable接口,即實現Delayed接口的對象必須實現getDelay(TimeUnit unit)方法和compareTo(T o)方法。這裏compareTo(T o)方法可以用來實現元素的排序,可以將延時時間長的放到隊列的末尾。
DelayQueue構造函數
上面分析了Delayed接口,接下來我們分析DelayQueue的構造函數。DelayQueue提供了2種構造函數,一個是無參構造函數,一個是給定集合爲參數的構造函數。其源碼如下:
/**
* 構建一個空的DelayQueue
*/
public DelayQueue() {}
/**
* 給定集合c爲參數的構造函數
* 將集合c中的元素全部放入到DelayQueue中
*/
public DelayQueue(Collection < ? extends E > c) {
this.addAll(c);
}
addAll方法是AbstractQueue抽象類中的方法,其源碼如下:
public boolean addAll(Collection < ? extends E > c) {
// 參數檢測
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
boolean modified = false;
//遍歷集合c中的元素
for (E e: c)
// 調用DelayQueue中的add方法
if (add(e))
modified = true;
return modified;
}
從上面的源碼中,我們可以看到,AbstractQueue抽象類中addAll方法實際是調用DelayQueue類中的add方法來實現的。
DelayQueue 入列操作
DelayQueue提供了4中入列操作,分別是:
- add(E e):阻塞的將制定元素添加到延時隊列中去,因爲隊列是***的因此此方法永不阻塞。
- offer(E e):阻塞的將制定元素添加到延時隊列中去,因爲隊列是***的因此此方法永不阻塞。
- put(E e):阻塞的將制定元素添加到延時隊列中去,因爲隊列是***的因此此方法永不阻塞。
- offer(E e, long timeout, TimeUnit unit):阻塞的將制定元素添加到延時隊列中去,因爲隊列是***的因此此方法永不阻塞。
這裏大家可能會奇怪,爲什麼這些入列方法的解釋都是一樣的?這個問題先等下回答,我們先來看看這幾個入列方法的源碼定義:
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
//獲取可重入鎖
final ReentrantLock lock = this.lock;
//加鎖
lock.lock();
try {
//調用PriorityQueue中的offer方法
q.offer(e);
//調用PriorityQueue中的peek方法
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
//釋放鎖
lock.unlock();
}
}
public void put(E e) {
offer(e);
}
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
這裏我們從源碼中可以看到,add(E e)方法、put(E e)方法和offer(E e,long timeout,TimeUnit unit)方法都是調用offer(E e)方法來實現的,這也是爲什麼這幾個方法的解釋都是一樣的原因。其中offer(E e)方法的核心又是調用了PriorityQueue中的offer(E e)方法,PriorityQueue和PriorityBlockingQueue都是以二叉堆的***隊列,只不過PriorityQueue不是阻塞的而PriorityBlockingQueue是阻塞的。
DelayQueue出列操作
DelayQueue提供了3中出列操作方法,它們分別是:
- poll():檢索並刪除此隊列的開頭,如果此隊列沒有延遲延遲的元素,則返回null
- take():檢索併除去此隊列的頭,如有必要,請等待直到該隊列上具有過期延遲的元素可用。
- poll(long timeout, TimeUnit unit):檢索並刪除此隊列的頭,如有必要,請等待直到該隊列上具有過期延遲的元素可用,或者或指定的等待時間到期。
下面我們來一個一個分析出列操作的原來。
poll():
poll操作的源碼定義如下:
public E poll() {
//獲取可重入鎖
final ReentrantLock lock = this.lock;
//加鎖
lock.lock();
try {
//獲取隊列中的第一個元素
E first = q.peek();
//若果元素爲null,或者頭元素還未過期,則返回false
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
//調用PriorityQueue中的出列方法
return q.poll();
} finally {
lock.unlock();
}
}
該方法與PriorityQueue的poll方法唯一的區別就是多了if (first == null || first.getDelay(NANOSECONDS) > 0)這個條件判斷,該條件是表示如果隊列中沒有元素或者隊列中的元素未過期,則返回null。
take
take操作源碼定義如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//加鎖
lock.lockInterruptibly();
try {
//西循環
for (;;) {
//查看隊列頭元素
E first = q.peek();
//如果隊列頭元素爲null,則表示隊列中沒有數據,線程進入等待隊列
if (first == null)
available.await();
else {
// 獲取first元素剩餘的延時時間
long delay = first.getDelay(NANOSECONDS);
//若果剩餘延時時間<=0 表示元素已經過期,可以從隊列中獲取元素
if (delay <= 0)
//直接返回頭部元素
return q.poll();
//若果剩餘延時時間>0,表示元素還未過期,則將first置爲null,防止內存溢出
first = null; // don't retain ref while waiting
//如果leader不爲null,則直接進入等待隊列中等待
if (leader != null)
available.await();
else {
//若果leader爲null,則把當前線程賦值給leader,並超時等待delay納秒
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();
}
}
take操作比poll操作稍微要複雜些,但是邏輯還是相對比較簡單。只是在獲取元素的時候先檢查元素的剩餘延時時間,如果剩餘延時時間<=0,則直接返回隊列頭元素。如果剩餘延時時間>0,則判斷leader是否爲null,若果leader不爲null,則表示已經有線程在等待獲取隊列的頭部元素,因此直接進入等待隊列中等待。若果leader爲null,則表示這是第一個獲取頭部元素的線程,把當前線程賦值給leader,然後超時等待剩餘延時時間。在take操作中需要注意的一點是fist=null,因爲如果first不置爲null的話會引起內存溢出的異常,這是因爲在併發的時候,每個線程都會持有一份first,因此first不會被釋放,若果線程數過多,就會導致內存溢出的異常。
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) {
if (nanos <= 0)
return null;
else
nanos = available.awaitNanos(nanos);
} else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();
if (nanos <= 0)
return null;
first = null; // don't retain ref while waiting
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
long timeLeft = available.awaitNanos(delay);
nanos -= delay - timeLeft;
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
這個出列操作的邏輯和take出列操作的邏輯幾乎一樣,唯一不同的在於take是無時間限制等待,而改操作是超時等待。
總結
DelayQueue的入列和出列操作邏輯相對比較簡單,就是在獲取元素的時候,判斷元素是否已經過期,若果過期就可以直接獲取,沒有過期的話poll
操作是直接返回null,take操作是進入等待隊列中等待。