深度解析延遲隊列DelayQueue

前言

有時候,我們有一些任務需要“稍後”來做,比如一些連接需要空閒一段時間後再關閉,session需要空閒一段時間後自動退出。這個時候就需要一些可以延遲執行任務的工具。DelayQueue(延遲隊列)就是一個可以實現類似功能的工具。

由於本篇會涉及到優先隊列PriorityQueue,所以預先閱讀 深度解析優先級隊列PriorityQueue 很有必要。

DelayQueue

DelayQueue(延遲隊列)的標準實現出現在JDK1.5中的J.U.C包中,作爲一個工具類,用來管理一些需要延遲處理的任務。先來看下它的類體系結構:
DelayQueue
需要注意的是DelayQueue實現了BlockingQueue接口,意味着它也是阻塞隊列,但是類名稱中沒有包含Blocking

示例演示

因爲DelayQueue比一般隊列使用起來稍微複雜一定,所以先看一個實例:

/**
 * @author sicimike
 */
public class DelayQueueDemo {

    public static void main(String[] args) {
        final long currentTime = System.currentTimeMillis();

        DelayQueue<DelayObject> queue = new DelayQueue();
        queue.offer(new DelayObject("Sic-001", currentTime + 6000L));
        queue.offer(new DelayObject("Sic-002", currentTime + 3000L));
        queue.offer(new DelayObject("Sic-003", currentTime + 8000L));

        System.out.println(System.currentTimeMillis() + " -> 入隊完成" );

        while (!queue.isEmpty()) {
            try {
                DelayObject delayObject = queue.take();
                System.out.println(System.currentTimeMillis() + " -> " + delayObject);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

其中隊列內部元素DelayObject定義如下:

/**
 * 延遲隊列中的元素
 * 必須實現Delayed接口
 * @author sicimike
 */
class DelayObject implements Delayed {

    // 過期時間
    private Long destroyTime;
    private String id;

    @Override
    public long getDelay(TimeUnit unit) {
    	// 返回元素剩餘的時間
        return unit.convert(destroyTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
    	// 剩餘時間少的,排在隊列前面
        return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
    }

    public DelayObject() {
    }

    public DelayObject(String id, Long destroyTime) {
        this.id = id;
        this.destroyTime = destroyTime;
    }

    public Long getDestroyTime() {
        return destroyTime;
    }

    public void setDestroyTime(Long destroyTime) {
        this.destroyTime = destroyTime;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "DelayObject {" +
                "destroyTime=" + destroyTime +
                ", id='" + id + '\'' +
                '}';
    }

}

執行結果:

1576763926966 -> 入隊完成
1576763929959 -> DelayObject {destroyTime=1576763929959, id='Sic-002'}
1576763932959 -> DelayObject {destroyTime=1576763932959, id='Sic-001'}
1576763934961 -> DelayObject {destroyTime=1576763934959, id='Sic-003'}

從執行結果可以看出,三個元素入隊完成後,大概延遲了3秒,出隊了第一個元素;繼續延遲3秒,出隊第二個元素;繼續延遲2秒,出隊第三個元素。
元素的出隊順序與放入的順序無關,而與元素延遲的時間有關,按照升序排列出隊,順序由DelayObject中的compareTo方法決定。
放入DelayQueue中的元素必須實現Delayed接口,實現接口中的getDelay方法和compareTo方法。getDelay獲取的是元素剩餘的時間,所以不同的時刻調用該方法,應該得到不同的值,否則元素永遠不會過期。compareTo方法用於給元素排序,排在前面的先出隊。
這就是延遲隊列DelayQueue的基本使用。

內部結構

大致瞭解DelayQueue的使用後,再來看下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;

    /**
     * Condition signalled when a newer element becomes available
     * at the head of the queue or a new thread may need to
     * become leader.
     */
    private final Condition available = lock.newCondition();
}

根據定義可以看出底層通過組合的方式持有優先隊列PriorityQueue的對象,也就是說延遲隊列DelayQueue底層是通過優先隊列PriorityQueue來實現的。

核心方法

需要重點關注的核心操作只有三個:入隊出隊查看隊首元素

offer(E e)方法

offer(E e)方法用於往DelayQueue中放入元素。由於DelayQueue是無界隊列(底層PriorityQueue可以自動擴容),所以往DelayQueue中放入元素是非阻塞的。putadd均是調用offer方法,其實現如下:

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

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

public boolean offer(E e, long timeout, TimeUnit unit) {
    return offer(e);
}

// 真正完成入隊操作的方法
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    // 加鎖
    lock.lock();
    try {
    	// 入隊(直接調用PriorityQueue的入隊方法)
        q.offer(e);
        if (q.peek() == e) {
        	// 放入的元素恰好是下一個要出隊的元素
        	// 喚醒因調用take方法而被阻塞的線程
        	// leader設置爲null是爲了使當前隊首元素快速進入設置了時間的阻塞狀態(結合take方法)
            leader = null;
            available.signal();
        }
        return true;
    } finally {
    	// 解鎖
        lock.unlock();
    }
}

take()方法

take()方法以阻塞的方式從DelayQueue中取出一個元素。

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)
                	// 時間到了,出隊
                    return q.poll();
                // 等待的時候不需要持有隊首元素引用    
                first = null; // don't retain ref while waiting
                if (leader != null)
                	// leader不爲null,表示有線程已經設置了阻塞時間,當前線程直接被阻塞
                    available.await();
                else {
                	// 沒有線程設置過阻塞時間(或者阻塞時間已經到了)
                    Thread thisThread = Thread.currentThread();
                    // 當前線程設置成leader線程
                    leader = thisThread;
                    try {
                    	// 因爲隊首元素還有delay時間才能出隊,所以休眠delay時間
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                        	// 休眠時間到了,釋放leader
                        	// 下一次循環出隊
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
        	// 前一個元素已經出隊,並且隊列中還有元素
        	// 喚醒其他被阻塞的線程
            available.signal();
        // 解鎖
        lock.unlock();
    }
}

通過take方法可以看出DelayQueue使用ReentrantLock + Condition來實現線程的阻塞、喚醒。再加上循環,如果隊首元素不出隊,線程會一直被阻塞。
這裏的leader使用的是Leader/Follower模型,隊首元素尚未出隊時,leader不爲null。其餘的線程知道leader不爲null之後就被無限阻塞;隊首元素出隊後,leader爲null,通知其餘的線程來爭搶。

總結

DelayQueue實現了BlockingQueue接口,是阻塞隊列。底層利用了優先隊列PriorityQueue的自動擴容、排序等機制。再利用ReentrantLock + Condition 機制實現線程安全,以及線程休眠、喚醒。

發佈了55 篇原創文章 · 獲贊 107 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章