源码解读之(六)DelayQueue

一、简介

DelayQueue是一个支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。也就是说只有在延迟期到时才能够从队列中取元素。

DelayQueue非常有用,可以运用在以下两个应用场景:

  • 缓存系统的设计:使用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,就表示有缓存到期了。
  • 定时任务调度:使用DelayQueue保存当天要执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如Timer就是使用DelayQueue实现的。

二、继承体系

在这里插入图片描述

从继承体系可以看到,DelayQueue实现了BlockingQueue,所以它是一个阻塞队列。

DelayQueue 继承了AbstractQueue,具有了队列的行为。

另外,DelayQueue还组合了一个叫做Delayed的接口,DelayQueue中存储的所有元素必须实现Delayed接口。

那么,Delayed是什么呢?

public interface Delayed extends Comparable<Delayed> {

    long getDelay(TimeUnit unit);
    
}

Delayed是一个继承自Comparable的接口,并且定义了一个getDelay()方法,用于表示还有多少时间到期,到期了应返回小于等于0的数值。

三、DelayQueue 数据结构

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;

    //条件控制,表示是否可以从队列中取数据
    private final Condition available = lock.newCondition();
}
  • lock:全局独占锁,用于实现线程安全
  • q:优先队列,用于存储元素,并按优先级排序
  • leader:用于优化内部阻塞通知的线程
  • available:用于实现阻塞的Condition对象

其实看到这里,我们应该已经能够了解DelayQueue的大致实现思路了:

以支持优先级的PriorityQueue无界队列作为一个容器,因为元素都必须实现Delayed接口,可以根据元素的过期时间来对元素进行排列,因此,先过期的元素会在队首,每次从队列里取出来都是最先要过期的元素。

1、构造方法

// 默认构造方法,这个简单,什么都没有
public DelayQueue() {}

// 通过集合初始化
public DelayQueue(Collection<? extends E> c) {
    this.addAll(c);
}

DelayQueue 内部组合PriorityQueue,对元素的操作都是通过PriorityQueue 来实现的,DelayQueue 的构造方法很简单,对于PriorityQueue 都是使用的默认参数,不能通过DelayQueue 来指定PriorityQueue的初始大小,也不能使用指定的Comparator,元素本身就需要实现Comparable ,因此不需要指定的Comparator。

2、入队

(1)、add(E e)

将指定的元素插入到此队列中,在成功时返回 true

public boolean add(E e) {
    return offer(e);
}

(2)、offer(E e)

将指定的元素插入到此队列中,在成功时返回 true,在前面的add 中,内部调用了offer 方法,我们也可以直接调用offer 方法来完成入队操作。

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 向 PriorityQueue中插入元素
        q.offer(e);
        // 如果当前元素的队首元素(优先级最高),leader设置为空,唤醒所有等待线程
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        // 无界队列,永远返回true
        return true;
    } finally {
        lock.unlock();
    }
}

peek并不一定是当前添加的元素,队头是当前添加元素,说明当前元素e的优先级最小也就即将过期的,这时候激活avaliable变量条件队列里面的一个线程,通知他们队列里面有元素了。

leader是等待获取队列头元素的线程,应用主从式设计减少不必要的等待。如果leader不等于空,表示已经有线程在等待获取队列的头元素。所以,通过await()方法让出当前线程等待信号。如果leader等于空,则把当前线程设置为leader,当一个线程为leader,它会使用awaitNanos()方法让当前线程等待接收信号或等待delay时间。

(3)、offer(E e, long timeout, TimeUnit unit)

public boolean offer(E e, long timeout, TimeUnit unit) {
    //调用offer 方法
    return offer(e);
}

因为是无界队列,因此不会出现”队满”(超出最大值会抛异常),指定一个等待时间将元素放入队列中并没有意义,队列没有达到最大值那么会入队成功,达到最大值,则失败,不会进行等待。

(4)、put(E e)

将指定的元素插入此队列中,队列达到最大值,则抛oom异常

public void put(E e) {
    offer(e);
}

虽然提供入队的接口方式很多,实际都是调用的offer 方法,通过PriorityQueue 来进行入队操作,入队超时方法并没有其超时功能。

3、出队

(1)、poll()

获取并移除此队列的头,如果此队列为空,则返回 null

public E poll() {
    final ReentrantLock lock = this.lock;
    //获取同步锁
    lock.lock();
    try {
        //获取队头
        E first = q.peek();
        //如果队头为null 或者 延时还没有到,则返回null
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            return q.poll(); //元素出队
    } finally {
        lock.unlock();
    }
}

(2)、poll(long timeout, TimeUnit unit)

获取并移除此队列的头部,在指定的等待时间前等待。

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    //超时等待时间
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    //可中断的获取锁
    lock.lockInterruptibly();
    try {
        //无限循环
        for (;;) {
            //获取队头元素
            E first = q.peek();
            //队头为空,也就是队列为空
            if (first == null) {
                //达到超时指定时间,返回null 
                if (nanos <= 0)
                    return null;
                else
                    // 如果还没有超时,那么再available条件上进行等待nanos时间
                    nanos = available.awaitNanos(nanos);
            } else {
                //获取元素延迟时间
                long delay = first.getDelay(NANOSECONDS);
                //延时到期
                if (delay <= 0)
                    return q.poll(); //返回出队元素
                //延时未到期,超时到期,返回null
                if (nanos <= 0)
                    return null;
                first = null; // don't retain ref while waiting
                // 超时等待时间 < 延迟时间 或者有其它线程再取数据
                if (nanos < delay || leader != null)
                    //在available 条件上进行等待nanos 时间
                    nanos = available.awaitNanos(nanos);
                else {
                    //超时等待时间 > 延迟时间 并且没有其它线程在等待,那么当前元素成为leader,表示leader 线程最早 正在等待获取元素
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                    	//等待  延迟时间 超时
                        long timeLeft = available.awaitNanos(delay);
                        //还需要继续等待 nanos
                        nanos -= delay - timeLeft;
                    } finally {
                        //清除 leader
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        //唤醒阻塞在available 的一个线程,表示可以取数据了
        if (leader == null && q.peek() != null)
            available.signal();
        //释放锁
        lock.unlock();
    }
}

来梳理梳理这里的逻辑:
1、如果队列为空,如果超时时间未到,则进行等待,否则返回null
2、队列不空,取出队头元素,如果延迟时间到,则返回元素,否则 如果超时 时间到 返回null
3、超时时间未到,并且超时时间< 延迟时间或者有线程正在获取元素,那么进行等待
4、超时时间> 延迟时间,那么肯定可以取到元素,设置leader为当前线程,等待延迟时间到期。

这里需要注意的时Condition 条件在阻塞时会释放锁,在被唤醒时会再次获取锁,获取成功才会返回。
当进行超时等待时,阻塞在Condition 上后会释放锁,一旦释放了锁,那么其它线程就有可能参与竞争,某一个线程就可能会成为leader(参与竞争的时间早,并且能在等待时间内能获取到队头元素那么就可能成为leader)
leader是用来减少不必要的竞争,如果leader不为空说明已经有线程在取了,设置当前线程等待即可。(leader 就是一个信号,告诉其它线程:你们不要再去获取元素了,它们延迟时间还没到期,我都还没有取到数据呢,你们要取数据,等我取了再说)
下面用流程图来展示这一过程:

在这里插入图片描述

(3)、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);
                // 如果小于0说明已到期,直接调用poll()方法弹出队首元素
                if (delay <= 0)
                    return q.poll();
                
                // 如果delay大于0 ,则下面要阻塞了
                
                // 将first置为空方便gc,因为有可能其它元素弹出了这个元素
                // 这里还持有着引用不会被清理
                first = null; // don't retain ref while waiting
                // 如果前面有其它线程在等待,直接进入等待
                if (leader != null)
                    available.await();
                else {
                    // 如果leader为null,把当前线程赋值给它
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 等待delay时间后自动醒过来
                        // 醒过来后把leader置空并重新进入循环判断队首元素是否到期
                        // 这里即使醒过来后也不一定能获取到元素
                        // 因为有可能其它线程先一步获取了锁并弹出了队首元素
                        // 条件锁的唤醒分成两步,先从Condition的队列里出队
                        // 再入队到AQS的队列中,当其它线程调用LockSupport.unpark(t)的时候才会真正唤醒
                        // 关于AQS我们后面会讲的^^
                        available.awaitNanos(delay);
                    } finally {
                        // 如果leader还是当前线程就把它置为空,让其它线程有机会获取元素
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // 成功出队后,如果leader为空且队首还有元素,就唤醒下一个等待的线程
        if (leader == null && q.peek() != null)
            // signal()只是把等待的线程放到AQS的队列里面,并不是真正的唤醒
            available.signal();
        // 解锁,这才是真正的唤醒
        lock.unlock();
    }
}

该方法就是相当于在前面的超时等待中,把超时时间设置为无限大,那么这样只要队列中有元素,要是元素延迟时间要求,那么就可以取出元素,否则就直接等待元素延迟时间到期,再取出元素,最先参与等待的线程会成为leader。

(4)、peek()

调用此方法,可以返回队头元素,但是元素并不出队。

public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //返回队列头部元素,元素不出队
        return q.peek();
    } finally {
        lock.unlock();
    }
}

四、案例Demo

源码虽然看了,但我们还是得动手来实践下,下面来看个案例Demo:

package com.concurrent;

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author riemann
 * @date 2019/09/01 1:38
 */
public class DelayQueueTest {

    public static void main(String[] args) {

        DelayQueue<Message> queue = new DelayQueue<>();

        long now = System.currentTimeMillis();

        // 启动一个线程从队列中取元素
        new Thread(()->{
            while (true) {
                try {
                    // 将依次打印1000,2000,5000,7000,8000
                    System.out.println(queue.take().deadline - now);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 添加5个元素到队列中
        queue.add(new Message(now + 5000));
        queue.add(new Message(now + 8000));
        queue.add(new Message(now + 2000));
        queue.add(new Message(now + 1000));
        queue.add(new Message(now + 7000));

    }

}

class Message implements Delayed {

    long deadline;

    public Message(long deadline) {
        this.deadline = deadline;
    }

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

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

    @Override
    public String toString() {
        return "Message{" +
                "deadline=" + deadline +
                '}';
    }

}

输出结果:

1000
2000
5000
7000
8000

由结果可以看出:越早到期的元素越先出队。

五、总结

  1. DelayQueue 内部通过组合PriorityQueue 来实现存储和维护元素顺序的。
  2. DelayQueue 存储元素必须实现Delayed 接口,通过实现Delayed 接口,可以获取到元素延迟时间,以及可以比较元素大小(Delayed 继承Comparable)
  3. DelayQueue 通过一个可重入锁来控制元素的入队出队行为
  4. DelayQueue 中leader 标识用于减少线程的竞争,表示当前有其它线程正在获取队头元素。
  5. PriorityQueue 只是负责存储数据以及维护元素的顺序,对于延迟时间取数据则是在DelayQueue 中进行判断控制的。
  6. DelayQueue 没有实现序列化接口
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章