DelayQueue講解

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有什麼特別的呢?

DelayQueue核心

DelayQueue核心即劃分了兩種等待者的角色

  • 普通等待者 線程
  • leader 線程

普通的比較簡單,一句話就能概括,一直等着,沒人叫我我不醒。
leader 角色相對來說負責一些,隊列中第一個元素什麼時候過期,我啥時候醒,當然也可能提前醒來:新加元素的過期時間比之前預計的醒來時間還早,這時候重新進入循環,嘗試取元素。


用法示例

DelayQueue 思想和原理我們都懂了,那怎麼用呢?
留白等催更。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章