Java 延時隊列 DelayQueue

概述

java延遲隊列提供了在指定時間才能獲取隊列元素的功能,隊列頭元素是最接近過期的元素。沒有過期元素的話,使用poll()方法會返回null值,超時判定是通過getDelay(TimeUnit.NANOSECONDS)方法的返回值小於等於0來判斷。延時隊列不能存放空元素。

應用場景

The core idea is as follows:

(1) Users place orders, save them to the database, and push the order and its expiration time into DelayQueue

(2) Start a thread that checks the expiration of an order. The thread uses the take () method of delayQueue to get the expired order. This method is a blocking method. If there is no expired order at present, the method will block and wait until the order is obtained and then continue to execute.

(3) When take () obtains an expired order, the thread queries the order in the database according to the ID of the acquired order and checks the order status. If it is unpaid, it changes the status to expired.

延時隊列實現了Iterator接口,但iterator()遍歷順序不保證是元素的實際存放順序。

延遲隊列數據結構

DelayQueue<E extends Delayed>的隊列元素需要實現Delayed接口,該接口類定義如下:


    private final transient ReentrantLock lock = new ReentrantLock();
    private final PriorityQueue<E> q = new PriorityQueue<E>();

    /**
     * Thread designated to wait for the element at the head of
     * the queue.  This variant of the Leader-Follower pattern
     * (http://www.cs.wustl.edu/~schmidt/POSA/POSA2/) serves to
     * minimize unnecessary timed waiting.  When a thread becomes
     * the leader, it waits only for the next delay to elapse, but
     * other threads await indefinitely.  The leader thread must
     * signal some other thread before returning from take() or
     * poll(...), unless some other thread becomes leader in the
     * interim.  Whenever the head of the queue is replaced with
     * an element with an earlier expiration time, the leader
     * field is invalidated by being reset to null, and some
     * waiting thread, but not necessarily the current leader, is
     * signalled.  So waiting threads must be prepared to acquire
     * and lose leadership while waiting.
     */
    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();
    ......
}

DelayedQuene的優先級隊列 PriorityQueue 使用的排序方式是隊列元素的compareTo方法,優先級隊列存放順序是從小到大的,所以隊列元素的compareTo方法影響了隊列的出隊順序。

若compareTo方法定義不當,會造成延時高的元素在隊頭,延時低的元素無法出隊。

類架構:

方法:


  
由Delayed定義可以得知,隊列元素需要實現getDelay(TimeUnit unit)方法和compareTo(Delayed o)方法, getDelay定義了剩餘到期時間,compareTo方法定義了元素排序規則,注意,元素的排序規則影響了元素的獲取順序,將在後面說明。

獲取隊列元素: 阻塞與非阻塞獲取

阻塞出隊列

當 first 元素還沒到出隊列的時間,就一直等待,直到返回。

    /**
     * Retrieves and removes the head of this queue, waiting if necessary
     * until an element with an expired delay is available on this queue.
     *
     * @return the head of this queue
     * @throws InterruptedException {@inheritDoc}
     */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek();

 //沒有元素,讓出線程,等待java.lang.Thread.State#WAITING
                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,這裏在等待的時候,釋放 ref。

// 其它線程在leader線程TIMED_WAITING期間,會進入等待狀態,這樣可以只有一個線程去等待到時喚醒,避免大量喚醒操作
                    if (leader != null)
                        available.await(); // 等待
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {

// 等待剩餘時間後,再嘗試獲取元素,在等待期間,由於leader是當前線程,所以其它線程會等待。
                            available.awaitNanos(delay);
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal(); // 喚醒等待線程
            lock.unlock();
        }
    }

非阻塞出隊列

當 first 元素還沒到出隊列的時間,就直接返回 null。

    /**
     * Retrieves and removes the head of this queue, or returns {@code null}
     * if this queue has no elements with an expired delay.
     *
     * @return the head of this queue, or {@code null} if this
     *         queue has no elements with an expired delay
     */
    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E first = q.peek();
            if (first == null || first.getDelay(NANOSECONDS) > 0)
                return null;
            else
                return q.poll();
        } finally {
            lock.unlock();
        }
    }

其中,PriorityQueue 的 poll() 方法如下:

    public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        E result = (E) queue[0];
        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            siftDown(0, x);
        return result;
    }

關於 PriorityQueue 的實現原理,我們下一篇中講。

由代碼我們可以看出,獲取元素時,總是判斷PriorityQueue隊列的隊首元素是否到期,若未到期,返回null,所以compareTo()的方法實現不當的話,會造成隊首元素未到期,當隊列中有到期元素卻獲取不到的情況。因此,隊列元素的compareTo方法實現需要注意。

代碼實踐示例


package i.juc

import java.lang.Thread.sleep
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.DelayQueue

/**
 * @author: Jack
 * 2020-05-27 16:44
 */

fun main(args: Array<String>) {
    val order1 = Order(3000, "Order1")
    val order2 = Order(5000, "Order2")
    val order3 = Order(10000, "Order3")

    val delayQueue = DelayQueue<Order>()
    delayQueue.add(order1)
    delayQueue.add(order2)
    delayQueue.add(order3)

    println("Order delay queue begin: ${now()} \n")

    while (delayQueue.size > 0) {
        val order = delayQueue.poll()
        if (null != order) {
            println("Order ${order.name} is out. ${now()}")
        }
        sleep(1000)
    }

}

fun now() = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))





package i.juc

import java.util.concurrent.Delayed
import java.util.concurrent.TimeUnit

/**
 * @author: Jack
 * 2020-05-27 16:45
 */
class Order : Delayed {
    var delayTime: Long
    var name: String

    constructor(delayTime: Long, name: String) {
        this.delayTime = System.currentTimeMillis() + if (delayTime > 0) delayTime else 0
        this.name = name
    }

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    override fun getDelay(unit: TimeUnit): Long {
        return delayTime - System.currentTimeMillis()
    }


    /**
     * Compares this object with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.
     */
    override fun compareTo(o: Delayed): Int {
        val order = o as Order
        val t = this.delayTime - order.delayTime
        return when {
            t > 0 -> 1
            t < 0 -> -1
            else -> 0
        }
    }

}



運行結果:

Order delay queue begin: 2020-05-27 17:33:08 

Order Order1 is out. 2020-05-27 17:33:11
Order Order2 is out. 2020-05-27 17:33:13
Order Order3 is out. 2020-05-27 17:33:18

需要注意的是 compareTo 的順序問題:修改compareTo方法 t > 0 爲 -1 後的運行結果: 在10秒之後幾乎同時取出。

參考資料

https://mp.weixin.qq.com/s/tM3QVIdNtPW3x0w--LRy3Q
https://www.cnblogs.com/hhan/p/10678466.html


Kotlin開發者社區

專注分享 Java、 Kotlin、Spring/Spring Boot、MySQL、redis、neo4j、NoSQL、Android、JavaScript、React、Node、函數式編程、編程思想、"高可用,高性能,高實時"大型分佈式系統架構設計主題。

High availability, high performance, high real-time large-scale distributed system architecture design

分佈式框架:Zookeeper、分佈式中間件框架等
分佈式存儲:GridFS、FastDFS、TFS、MemCache、redis等
分佈式數據庫:Cobar、tddl、Amoeba、Mycat
雲計算、大數據、AI算法
虛擬化、雲原生技術
分佈式計算框架:MapReduce、Hadoop、Storm、Flink等
分佈式通信機制:Dubbo、RPC調用、共享遠程數據、消息隊列等
消息隊列MQ:Kafka、MetaQ,RocketMQ
怎樣打造高可用系統:基於硬件、軟件中間件、系統架構等一些典型方案的實現:HAProxy、基於Corosync+Pacemaker的高可用集羣套件中間件系統
Mycat架構分佈式演進
大數據Join背後的難題:數據、網絡、內存和計算能力的矛盾和調和
Java分佈式系統中的高性能難題:AIO,NIO,Netty還是自己開發框架?
高性能事件派發機制:線程池模型、Disruptor模型等等。。。

合抱之木,生於毫末;九層之臺,起於壘土;千里之行,始於足下。不積跬步,無以至千里;不積小流,無以成江河。

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