图解DelayQueue源码(java 8)——延时队列的小九九

DelayQueue 是一种特殊的阻塞队列,只有到期的对象,才能从队列中取出。

底层有用到 PriorityQueue,入队时会进行排序。也就是说,这个阻塞队列是有序的。

典型的应用场景比如:12306订票,30分钟内未支付,则取消订单。

实现这样的功能,用定时任务是刷,当然可以。但用DelayQueue会更精确。

一、示例代码


    public static void main(String[] args) throws Exception {
        long base = System.currentTimeMillis();
        DelayQueue<Food> queue = new DelayQueue<>();
        for(int i = 0; i < 10; i++){
            String name = "food_" + i;
            int cookedMinutes = RandomUtil.randomInt(1,20);
            Food food = new Food(name, cookedMinutes);
            log.info("name:{}, cookeMinutes:{}",name, cookedMinutes);
            queue.offer(food);
        }
        log.info("all foods OK");
        for(int i = 0; i < 10; i++){
            Food food = queue.take();
            log.info("foodName:{}, time:{}",food.name,(food.cookedSeconds - base) / 1000 );
        }


	static class Food  implements  Delayed{

        private String name;

        private long cookedSeconds;

        Food(String name, int cookedSeconds){
            this.name = name;
            this.cookedSeconds = System.currentTimeMillis() + cookedSeconds * 1000;

        }

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

        @Override
        public int compareTo(Delayed o) {
            long result = this.getDelay(TimeUnit.SECONDS) - o.getDelay(TimeUnit.SECONDS);
            return result == 0 ? 0 : result > 0 ? 1 : -1;
        }
    }

这个示例代码,你可以理解为涮火锅。

各种食材,煮熟所需要的时间是不同的。

吃的时候,只捞煮熟的。
在这里插入图片描述
只有实现了Delayed的接口,这样的元素才能放入 DelayQueue

在示例代码中重写了两个方法 getDelay() 。只有这个方法返回值 小于等于 0,才会出队。

重写 compareTo 方法,是给 PriorityQueue 排序用。

你可以运行下示例代码,看下效果。可以实现,类似吃火锅时,只捞起煮熟的菜。

二、入队操作


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

讲入队源码之前,先看下 DelayQueue 中的几个成员变量。

ReentrantLock lock是个全局锁,相关操作的时候,需要加锁。

PriorityQueue<E> q,这是个优先级队列,存放元素的。之前在介绍 PriorityBlockingQueue 时,详细说过堆化、排序等问题。

Thread leader,这个很有意思,英文好的,看下源码的注释,这个是用来控制出队竞争的。

Condition available,这个是用来阻塞与唤醒的。详细的分析,可以看我以前的博文:《ArrayBlockingQueue 源码解析


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

从入队源码来看,入队操作,不会阻塞。在加锁的情况下,元素被放入 PriorityQueue 中。

PriorityQueue 是会自动扩容的,自动排序的。

q.peek() == e 进行这个判断是做什么的?

还是拿刚刚吃火锅的例子:

你先在锅里放了些土豆,一时半会儿熟不了。

这时你又往里放了盘青菜。青菜虽然比土豆放的晚,可它会先熟。

你得吆喝两声,来,来,来,捞青菜吃。

q.peek() == e 这个的意思是所放元素,会第一个出队。 相当于青菜

leader = nullavailable.signal(),是通知出队线程,取元素。相当于那两声吆喝

三、出队操作


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

出队的逻辑,可以用刚刚吃火锅的例子,形象的讲一下。

首先,夹菜必需用公筷,公筷只有一双哈。

其中一个人,拿着公筷夹起一片肥牛,看成色不熟,就开始涮几下。

这时,其它几个人,要是也想夹菜,那得排队等着,因为公筷就那一双。

那人涮了几个后,又夹起来,看那肉熟了。肉夹到自己碗里,放下公筷。

这时,他吆喝一声,下一个。那别人就用这个公筷夹菜了。

这里的公筷,就相当于代码中的 leader,

leader 为空,可以取元素。

leader不为空,说明有线程在等着出队,其它线程调用 await() 方法排队等待。

await() 方法,先进入等待队列,再解锁、之后阻塞、被唤醒后抢锁。

signal() 方法,是从等待队列中取一个节点,放入CLH 队列,之后有机会去抢锁。

这两个方法的源码解析,看我之前的这篇博文《ArrayBlockingQueue 源码解析

有上面的铺垫,咱们再开始细说,出队源码。

假设有 t1, t2, t3, t4 这四个线程,同时来执行take() 方法。

首先,是 lock.lockInterruptibly(); 这一行是抢锁,假设 t1 抢到了锁。

t2, t3, t4 阻塞在这一行。效果如图。(AQS 框架不熟悉的话,看下这篇博文《ReentrantLock源码解析》)
在这里插入图片描述
假设 first == null ,那 t1 线程,会执行 available.await(),它会入等待队列,释放锁,阻塞。

在 t1 进入等待队列后,释放锁之前,会是下面这个样子。
在这里插入图片描述

当 t1 释放了锁,那么t2, t3, t4 去抢锁,某个线程抢到了锁,和 t1 线程一个命运。

会执行 available.await() 入队,释放锁,阻塞。 如此往复,最终四个线程都会阻塞到这行代码。
在这里插入图片描述

假如说 t5 线程,现在执行了 offer(E e) 方法,队列中有元素了,那会调用 available.signal() 这行。

那么会从等待队列中取出一个节点,放到CLH队列中抢锁。
在这里插入图片描述
t5 线程 释放锁的时候,会唤醒 CLH 队列中 最前面的节点。

那原来 t1 节点阻塞在 available.await(); 唤醒后,拿到锁,会跳出这行代码

比如在执行 long delay = first.getDelay(NANOSECONDS); 这行时,delay 大于 0 。

相当于肥牛还没熟,那就继续往下执行,leader = thisThread; 相当于拿到公筷啦。

执行 available.awaitNanos(delay); 比如还有5秒到时间,那它就等待5秒。

available.awaitNanos(delay) 方法和 available.await() 类似,

进入等待队列,解锁,限时阻塞,自我唤醒

限时阻塞,指阻塞一定时间后,自动唤醒自己,进入 CLH 队列,抢到锁,跳出 awaitNanos() 方法。

若是在 t1 限时阻塞期间,有别的线程来执行 take() 方法, 执行 leader != null 时,会返回true,

因为公筷是t1 拿着,其它线程即使来了,也会阻塞在 available.await(); 这行代码中。

当 t1 线程限时阻塞结束、自我唤醒拿到锁,就跳出 awaitNanos

此时阻塞队列中,是有可以取走的元素,之前是肥牛没熟,现在熟了……

t1 丢掉了公筷,第二次进入 for 循环,会取走上次没有取走的肥牛。

在 return 之前,执行 finally中的代码


	if (leader == null && q.peek() != null) // 锅里有肉,公筷没有被占用
               available.signal(); // 唤醒一个节点,让它去抢锁
           lock.unlock(); // 释放锁
           

至此为此,take() 方法讲完了,这里面哪里阻塞,怎么被唤醒的,都说了下。

代码逻辑不复杂,这里面并发控制的真好,阻塞也设计的很好,尤其是公筷的设计,特别棒。

Doug Lea 真的是大牛,写的代码太牛了!

至此,阻塞队列中,常用的五个阻塞队列,源码逐一做了分析。

包括,

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue
  • SynchronousQueue
  • DelayQueue

对于不同的场景,选择不同的阻塞队列,我也写了一篇总括的文章。《浅谈五种常用的阻塞队列》。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的DelayQueue是一个特殊的队列,它只允许在指定的间之后才能从队列中取出元素。可以使用DelayQueue来实现一些迟任务的功能,例如任务调度、缓存过期等。 DelayQueue基于PriorityQueue实现,但是它的元素必须实现Delayed接口,Delayed接口中定义了一个getDelay()方法,返回元素的间。 当从DelayQueue中取出元素,如果该元素的间还没有到达,则该元素会被重新加入队列中,直到间到达。 以下是一个简单的使用DelayQueue的例子: ```java import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; public class DelayQueueExample { public static void main(String[] args) throws InterruptedException { DelayQueue<DelayedElement> delayQueue = new DelayQueue<DelayedElement>(); delayQueue.add(new DelayedElement("element1", 2000)); delayQueue.add(new DelayedElement("element2", 5000)); delayQueue.add(new DelayedElement("element3", 1000)); while (!delayQueue.isEmpty()) { DelayedElement element = delayQueue.take(); System.out.println("Taking element: " + element); } } static class DelayedElement implements Delayed { private String name; private long delayTime; public DelayedElement(String name, long delayTime) { this.name = name; this.delayTime = System.currentTimeMillis() + delayTime; } @Override public long getDelay(TimeUnit unit) { long diff = delayTime - System.currentTimeMillis(); return unit.convert(diff, TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { if (this.delayTime < ((DelayedElement) o).delayTime) { return -1; } if (this.delayTime > ((DelayedElement) o).delayTime) { return 1; } return 0; } @Override public String toString() { return "DelayedElement{" + "name='" + name + '\'' + ", delayTime=" + delayTime + '}'; } } } ``` 在上面的例子中,我们创建了一个DelayQueue,并向其中添加了三个DelayedElement元素。每个元素都有一个间,分别为2秒、5秒和1秒。 在主线程中,我们不断地从DelayQueue中取出元素,直到队列为空。当元素的间还没有到达,它会被重新加入队列中,直到间到达。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值