DelayQueue源码分析

DelayQueue

DelayQueue是一个无界的阻塞队列,它的元素实现了Delayed接口,并且元素只有在delay到期以后才能取出。队列头是延迟过期时间最长的元素,如果没有元素过期,执行poll()方法返回null。

案例

首先定义一个实现Delayed接口的泛型类MyDelayNode,Delayed接口继承了Comparable接口。

public class MyDelayNode<T> implements Delayed {

    private int delay;  //过期时间
    private TimeUnit unit; //单位
    private long current;

    private T item;  //实际的数据元素

    public MyDelayNode(T item, int delay, TimeUnit unit) {
        this.item = item;
        this.delay = delay;
        this.unit = unit;
        this.current = System.currentTimeMillis();
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return current + this.unit.toMillis(delay) - System.currentTimeMillis();
    }

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

    @Override
    public String toString() {
        return "MyDelayNode{" +
                "item=" + item +
                '}';
    }
}

测试demo类

public class DelayQueueDemo {

    private static final Logger logger = LoggerFactory.getLogger(DelayQueueDemo.class);

    public static void main(String[] args) throws InterruptedException {

        DelayQueue<MyDelayNode<String>> delayQueue = new DelayQueue<>();

        for (int i = 0; i < 5; i++) {   //添加5个元素到队列
            delayQueue.put(new MyDelayNode<>("节点" + (i + 1), i + 1, TimeUnit.SECONDS));
        }

        logger.info("====================================");

        while (delayQueue.peek() != null) {   //循环取出元素
            logger.info(delayQueue.take() + "");
        }
    }
}

运行结果:

10:28:44.888 [main] INFO com.example.testdemo56.DelayQueueDemo - ====================================
10:28:45.886 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点1}
10:28:46.885 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点2}
10:28:47.885 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点3}
10:28:48.885 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点4}
10:28:49.885 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点5}

从测试结果可以看出,5个元素间隔1s取出。

源码

先来看一下类中定义的几个字段。

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();

这几个字段都比较重要,保证线程安全的锁和阻塞线程的条件很好理解,这是阻塞队列的特点决定的。主要是这个优先队列q和线程leader,我们可以看出DelayQueue底层通过优先队列实现的,因为DelayQueue也是有顺序的,只是这个顺序是按delay时间排序的。那这个leader是做什么的呢?我们继续向下看。

因为DelayQueue也是实现的BlockingQueue接口,这里还是关注与阻塞有关的put和take方法。

先来看put方法,put方法内部调用的是offer方法,这很好理解因为DelayQueue无界,所有无界的阻塞队列都是这样实现的,因为不可能满,所以插入时不可能阻塞。

public void put(E e) {
    offer(e);
}
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的offer方法插入元素,由于优先队列也是无界的,所以这里不会出现阻塞。只是插入以后判断了一下插入的元素是不是头节点,如果是执行了两行代码,这两行代码我们后面介绍。

再来看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);
                if (delay <= 0)  //delay过期
                    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();
    }
}

单看代码,这个方法并不复杂,但是这里存在一定的逻辑,我们要理解的是这个代码为什么这么写。

我们都知道,对于阻塞队列来说,执行take方法时,如果队列为空,会阻塞,不为空,理解返回队列头元素。但是DelayQueue存在一种特殊情况,就是队列不为空,但是Delay接口的getDelay()方法返回的值大于0,这是线程同样会等待,直到Delay接口的getDelay()方法返回的值小于0才能返回队列头。

知道了这个前提,我们再来分析代码。

首先第5行是一个死循环,关注后面的代码,只有第11行满足才能终止循环,这是唯一的出口。死循环下面的3行代码,很好理解,如果队列为空,线程阻塞。这个不需要解释。如果队列不为空,同样存在两种情况。

这里我们举例来分析,假设当前队列存在5个元素,delay时间(Delay接口的getDelay()方法返回的值)分别为2,3,4,5,6,这样队列头就是delay时间为2的,并且队列头delay时间没有过期。现在存在3个线程同时调用take方法,来获取队列头。我们来分析这个过程。

  1. 假设线程1争抢到了lock锁,执行到第10行时,发现delay>0,肯定无法立即返回
  2. 判断leader是否为null,此时leader为null
  3. 将当前线程赋予leader,然后线程阻塞delay,释放锁
  4. 线程2获取锁,发现leader不为null,立即阻塞,释放锁
  5. 线程3获取锁,与线程2行为一样
  6. 线程1阻塞时间到达后,会将leader置为null,在死循环中执行到第10行,此时delay<0,退出循环,在返回队列头之前,会执行finally块中通知线程唤醒得代码
  7. 假设线程2被唤醒,重复执行上述1-6

这里的leader线程代表着等待队列头时间到达的线程,leader线程永远等待队列头的超时时间,除了leader线程的其他线程均无限等待直到收到唤醒通知,才有机会变成leader线程。

offer方法的这三行代码解释一下。

if (q.peek() == e) { 
    leader = null;
    available.signal();
}

当插入的元素在队列头时,肯定会打断原有等待队列头的线程leader,然后在take方法中的死循环重新选出leader,等待队列头时候超时。

结论

DelayQueue类一个非常有用的特性是,在生产者-消费者模型中,只有在特定元素过期后才能将他们消费。另一个用途在于线程池,提交任务以后延时执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
DelayQueue是一个通过PriorityBlockingQueue实现延迟获取元素的无界阻塞队列。在DelayQueue中添加的元素必须实现Delayed接口,并且只有在延迟期满后才能从队列中提取元素。\[2\]DelayQueue常用于需要延迟处理任务的场景,比如在网上商城下单后,如果超时未支付,订单会被后台系统关闭,这种需要延时处理的场景可以使用DelayQueue来实现。\[3\]DelayQueue的实现原理是基于PriorityBlockingQueue,它是一个无界阻塞队列,可以按照元素的优先级进行排序。\[2\]DelayQueue源码定义如下:public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> { // ... }\[3\] #### 引用[.reference_title] - *1* *2* [延迟队列DelayQueue原理](https://blog.csdn.net/c15158032319/article/details/118636233)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Java 延迟队列 DelayQueue 的原理](https://blog.csdn.net/piaoranyuji/article/details/124042408)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值