图解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;
        }
    }

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

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

吃的时候,只捞煮熟的。
图解DelayQueue源码(java 8)——延时队列的小九九_第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 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源码解析》)
图解DelayQueue源码(java 8)——延时队列的小九九_第2张图片
假设 first == null ,那 t1 线程,会执行 available.await(),它会入等待队列,释放锁,阻塞。

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

图解DelayQueue源码(java 8)——延时队列的小九九_第3张图片

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

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

图解DelayQueue源码(java 8)——延时队列的小九九_第4张图片

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

那么会从等待队列中取出一个节点,放到CLH队列中抢锁。图解DelayQueue源码(java 8)——延时队列的小九九_第5张图片
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

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

你可能感兴趣的:(并发编程,DelayQueue,延时队列,阻塞队列,BlockingQueue)