DelayQueue

  DelayQueue是一个内部依靠AQS队列同步器所实现的无界延迟阻塞队列

       DelayQueue队列中每个元素都有个过期时间,并且队列是个优先级队列,当从队列获取元素时候,只有过期元素才会出队。

       延迟对象需要覆盖 getDelay()与compareTo()方法,并且要注意 getDelay()的时间单位的统一,getDelay定义剩余到期时间,compareTo方法定义了元素排序规则

  DelayQueue中内部使用的是PriorityQueue存放数据,使用ReentrantLock实现线程同步,可知是阻塞队列。另外队列里面的元素要实现Delayed接口,一个是获取当前剩余时间的接口,一个是元素比较的接口,因为这个是有优先级的队列。

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。
    private final Condition available = lock.newCondition();
}

  Thread 指定线程等待在队列头部的元素,成员变量Thead leader设计出来是为了minimize unnecessary timed waiting(减少不必要的等待时间), 在DelayQueue中leader表示一个等待从队列中获取消息的线程。

       元素入队

  当线程put元素的时候,DelayQueue会对你put的元素通过其本身的compareTo方法进行排序,延时时间越短的顺序越靠近队列头部

    /**
     * Inserts the specified element into this delay queue.
     *
     * @param e the element to add
     * @return {@code true}
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            q.offer(e);
            if (q.peek() == e) {
                leader = null;
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

  首先获取独占锁,然后添加元素到优先级队列,由于q是优先级队列,所以添加完元素后,peek()方法返回的并不一定是刚才添加的元素,如果判断为true,说明当前元素e的优先级最小也就是即将过期的,这时候激活avaliable变量条件队列里面的线程,通知它们队列里面有元素。

从队列中取元素

  poll()方法取队头当队头元素没过期时返回null,take()方法取队头当队头元素没过期时会一直等待。

  当线程take元素的时候,DelayQueue会检测当前是否有Thread已经在等待队头元素,如果有的话,那么只能阻塞当前前程,等已经取到队头的Thread完成以后再唤醒。 如果没有Thread在等待队头元素的话,那么会查询一下队头元素还剩多少Delay时间,并且将当前线程设置为队头等待线程,然后让当前线程wait剩余Delay时间后在来获取队头元素。

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        E first = q.peek();
        //如果队列为空,或者不为空但是队头元素没有过期则返回null
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            return q.poll();
    } finally {
        lock.unlock();
    }
}


/**
     * 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();
                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)
                        available.await();
                    else {
                        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();
        }
    }

使用场景

1)关闭空闲连接。服务器中,有很多客户端的连接,空闲一段时间之后需要关闭之。

2)缓存。缓存中的对象,超过了空闲时间,需要从缓存中移出。

3)任务超时处理。在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求。

4)模拟订单自动取消功能。

5)实现延时消息队列(简易版MQ)。

6)缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询。 DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。

7)定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。

8)典型场景是重试机制实现,比如当调用接口失败后,把当前调用信息放入delay=10s的元素,然后把元素放入队列,那么这个队列就是一个重试队列,一个线程通过take()方法获取需要重试的接口,take()返回则接口进行重试,失败则再次放入队列,同时也可以在元素加上重试次数。

 

  

posted on 2021-12-11 17:12  溪水静幽  阅读(147)  评论(0)    收藏  举报