源碼解讀之(六)DelayQueue

一、簡介

DelayQueue是一個支持延時獲取元素的無界阻塞隊列。裏面的元素全部都是“可延期”的元素,列頭的元素是最先“到期”的元素,如果隊列裏面沒有元素到期,是不能從列頭獲取元素的,哪怕有元素也不行。也就是說只有在延遲期到時才能夠從隊列中取元素。

DelayQueue非常有用,可以運用在以下兩個應用場景:

  • 緩存系統的設計:使用DelayQueue保存緩存元素的有效期,使用一個線程循環查詢DelayQueue,一旦能從DelayQueue中獲取元素時,就表示有緩存到期了。
  • 定時任務調度:使用DelayQueue保存當天要執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,比如Timer就是使用DelayQueue實現的。

二、繼承體系

在這裏插入圖片描述

從繼承體系可以看到,DelayQueue實現了BlockingQueue,所以它是一個阻塞隊列。

DelayQueue 繼承了AbstractQueue,具有了隊列的行爲。

另外,DelayQueue還組合了一個叫做Delayed的接口,DelayQueue中存儲的所有元素必須實現Delayed接口。

那麼,Delayed是什麼呢?

public interface Delayed extends Comparable<Delayed> {

    long getDelay(TimeUnit unit);
    
}

Delayed是一個繼承自Comparable的接口,並且定義了一個getDelay()方法,用於表示還有多少時間到期,到期了應返回小於等於0的數值。

三、DelayQueue 數據結構

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();
}
  • lock:全局獨佔鎖,用於實現線程安全
  • q:優先隊列,用於存儲元素,並按優先級排序
  • leader:用於優化內部阻塞通知的線程
  • available:用於實現阻塞的Condition對象

其實看到這裏,我們應該已經能夠了解DelayQueue的大致實現思路了:

以支持優先級的PriorityQueue無界隊列作爲一個容器,因爲元素都必須實現Delayed接口,可以根據元素的過期時間來對元素進行排列,因此,先過期的元素會在隊首,每次從隊列裏取出來都是最先要過期的元素。

1、構造方法

// 默認構造方法,這個簡單,什麼都沒有
public DelayQueue() {}

// 通過集合初始化
public DelayQueue(Collection<? extends E> c) {
    this.addAll(c);
}

DelayQueue 內部組合PriorityQueue,對元素的操作都是通過PriorityQueue 來實現的,DelayQueue 的構造方法很簡單,對於PriorityQueue 都是使用的默認參數,不能通過DelayQueue 來指定PriorityQueue的初始大小,也不能使用指定的Comparator,元素本身就需要實現Comparable ,因此不需要指定的Comparator。

2、入隊

(1)、add(E e)

將指定的元素插入到此隊列中,在成功時返回 true

public boolean add(E e) {
    return offer(e);
}

(2)、offer(E e)

將指定的元素插入到此隊列中,在成功時返回 true,在前面的add 中,內部調用了offer 方法,我們也可以直接調用offer 方法來完成入隊操作。

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    // 獲取鎖
    lock.lock();
    try {
        // 向 PriorityQueue中插入元素
        q.offer(e);
        // 如果當前元素的隊首元素(優先級最高),leader設置爲空,喚醒所有等待線程
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        // 無界隊列,永遠返回true
        return true;
    } finally {
        lock.unlock();
    }
}

peek並不一定是當前添加的元素,隊頭是當前添加元素,說明當前元素e的優先級最小也就即將過期的,這時候激活avaliable變量條件隊列裏面的一個線程,通知他們隊列裏面有元素了。

leader是等待獲取隊列頭元素的線程,應用主從式設計減少不必要的等待。如果leader不等於空,表示已經有線程在等待獲取隊列的頭元素。所以,通過await()方法讓出當前線程等待信號。如果leader等於空,則把當前線程設置爲leader,當一個線程爲leader,它會使用awaitNanos()方法讓當前線程等待接收信號或等待delay時間。

(3)、offer(E e, long timeout, TimeUnit unit)

public boolean offer(E e, long timeout, TimeUnit unit) {
    //調用offer 方法
    return offer(e);
}

因爲是無界隊列,因此不會出現”隊滿”(超出最大值會拋異常),指定一個等待時間將元素放入隊列中並沒有意義,隊列沒有達到最大值那麼會入隊成功,達到最大值,則失敗,不會進行等待。

(4)、put(E e)

將指定的元素插入此隊列中,隊列達到最大值,則拋oom異常

public void put(E e) {
    offer(e);
}

雖然提供入隊的接口方式很多,實際都是調用的offer 方法,通過PriorityQueue 來進行入隊操作,入隊超時方法並沒有其超時功能。

3、出隊

(1)、poll()

獲取並移除此隊列的頭,如果此隊列爲空,則返回 null

public E poll() {
    final ReentrantLock lock = this.lock;
    //獲取同步鎖
    lock.lock();
    try {
        //獲取隊頭
        E first = q.peek();
        //如果隊頭爲null 或者 延時還沒有到,則返回null
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            return q.poll(); //元素出隊
    } finally {
        lock.unlock();
    }
}

(2)、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) {
                //達到超時指定時間,返回null 
                if (nanos <= 0)
                    return null;
                else
                    // 如果還沒有超時,那麼再available條件上進行等待nanos時間
                    nanos = available.awaitNanos(nanos);
            } else {
                //獲取元素延遲時間
                long delay = first.getDelay(NANOSECONDS);
                //延時到期
                if (delay <= 0)
                    return q.poll(); //返回出隊元素
                //延時未到期,超時到期,返回null
                if (nanos <= 0)
                    return null;
                first = null; // don't retain ref while waiting
                // 超時等待時間 < 延遲時間 或者有其它線程再取數據
                if (nanos < delay || leader != null)
                    //在available 條件上進行等待nanos 時間
                    nanos = available.awaitNanos(nanos);
                else {
                    //超時等待時間 > 延遲時間 並且沒有其它線程在等待,那麼當前元素成爲leader,表示leader 線程最早 正在等待獲取元素
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                    	//等待  延遲時間 超時
                        long timeLeft = available.awaitNanos(delay);
                        //還需要繼續等待 nanos
                        nanos -= delay - timeLeft;
                    } finally {
                        //清除 leader
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        //喚醒阻塞在available 的一個線程,表示可以取數據了
        if (leader == null && q.peek() != null)
            available.signal();
        //釋放鎖
        lock.unlock();
    }
}

來梳理梳理這裏的邏輯:
1、如果隊列爲空,如果超時時間未到,則進行等待,否則返回null
2、隊列不空,取出隊頭元素,如果延遲時間到,則返回元素,否則 如果超時 時間到 返回null
3、超時時間未到,並且超時時間< 延遲時間或者有線程正在獲取元素,那麼進行等待
4、超時時間> 延遲時間,那麼肯定可以取到元素,設置leader爲當前線程,等待延遲時間到期。

這裏需要注意的時Condition 條件在阻塞時會釋放鎖,在被喚醒時會再次獲取鎖,獲取成功纔會返回。
當進行超時等待時,阻塞在Condition 上後會釋放鎖,一旦釋放了鎖,那麼其它線程就有可能參與競爭,某一個線程就可能會成爲leader(參與競爭的時間早,並且能在等待時間內能獲取到隊頭元素那麼就可能成爲leader)
leader是用來減少不必要的競爭,如果leader不爲空說明已經有線程在取了,設置當前線程等待即可。(leader 就是一個信號,告訴其它線程:你們不要再去獲取元素了,它們延遲時間還沒到期,我都還沒有取到數據呢,你們要取數據,等我取了再說)
下面用流程圖來展示這一過程:

在這裏插入圖片描述

(3)、take()

獲取並移除此隊列的頭部,在元素變得可用之前一直等待

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);
                // 如果小於0說明已到期,直接調用poll()方法彈出隊首元素
                if (delay <= 0)
                    return q.poll();
                
                // 如果delay大於0 ,則下面要阻塞了
                
                // 將first置爲空方便gc,因爲有可能其它元素彈出了這個元素
                // 這裏還持有着引用不會被清理
                first = null; // don't retain ref while waiting
                // 如果前面有其它線程在等待,直接進入等待
                if (leader != null)
                    available.await();
                else {
                    // 如果leader爲null,把當前線程賦值給它
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 等待delay時間後自動醒過來
                        // 醒過來後把leader置空並重新進入循環判斷隊首元素是否到期
                        // 這裏即使醒過來後也不一定能獲取到元素
                        // 因爲有可能其它線程先一步獲取了鎖並彈出了隊首元素
                        // 條件鎖的喚醒分成兩步,先從Condition的隊列裏出隊
                        // 再入隊到AQS的隊列中,當其它線程調用LockSupport.unpark(t)的時候纔會真正喚醒
                        // 關於AQS我們後面會講的^^
                        available.awaitNanos(delay);
                    } finally {
                        // 如果leader還是當前線程就把它置爲空,讓其它線程有機會獲取元素
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // 成功出隊後,如果leader爲空且隊首還有元素,就喚醒下一個等待的線程
        if (leader == null && q.peek() != null)
            // signal()只是把等待的線程放到AQS的隊列裏面,並不是真正的喚醒
            available.signal();
        // 解鎖,這纔是真正的喚醒
        lock.unlock();
    }
}

該方法就是相當於在前面的超時等待中,把超時時間設置爲無限大,那麼這樣只要隊列中有元素,要是元素延遲時間要求,那麼就可以取出元素,否則就直接等待元素延遲時間到期,再取出元素,最先參與等待的線程會成爲leader。

(4)、peek()

調用此方法,可以返回隊頭元素,但是元素並不出隊。

public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //返回隊列頭部元素,元素不出隊
        return q.peek();
    } finally {
        lock.unlock();
    }
}

四、案例Demo

源碼雖然看了,但我們還是得動手來實踐下,下面來看個案例Demo:

package com.concurrent;

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author riemann
 * @date 2019/09/01 1:38
 */
public class DelayQueueTest {

    public static void main(String[] args) {

        DelayQueue<Message> queue = new DelayQueue<>();

        long now = System.currentTimeMillis();

        // 啓動一個線程從隊列中取元素
        new Thread(()->{
            while (true) {
                try {
                    // 將依次打印1000,2000,5000,7000,8000
                    System.out.println(queue.take().deadline - now);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 添加5個元素到隊列中
        queue.add(new Message(now + 5000));
        queue.add(new Message(now + 8000));
        queue.add(new Message(now + 2000));
        queue.add(new Message(now + 1000));
        queue.add(new Message(now + 7000));

    }

}

class Message implements Delayed {

    long deadline;

    public Message(long deadline) {
        this.deadline = deadline;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return deadline - System.currentTimeMillis();
    }

    @Override
    public int compareTo(Delayed o) {
        return (int) (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
    }

    @Override
    public String toString() {
        return "Message{" +
                "deadline=" + deadline +
                '}';
    }

}

輸出結果:

1000
2000
5000
7000
8000

由結果可以看出:越早到期的元素越先出隊。

五、總結

  1. DelayQueue 內部通過組合PriorityQueue 來實現存儲和維護元素順序的。
  2. DelayQueue 存儲元素必須實現Delayed 接口,通過實現Delayed 接口,可以獲取到元素延遲時間,以及可以比較元素大小(Delayed 繼承Comparable)
  3. DelayQueue 通過一個可重入鎖來控制元素的入隊出隊行爲
  4. DelayQueue 中leader 標識用於減少線程的競爭,表示當前有其它線程正在獲取隊頭元素。
  5. PriorityQueue 只是負責存儲數據以及維護元素的順序,對於延遲時間取數據則是在DelayQueue 中進行判斷控制的。
  6. DelayQueue 沒有實現序列化接口
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章