Java DelayQueue延时阻塞队列源码深度解析

·  阅读 438

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第14天,点击查看活动详情

基于JDK1.8详细介绍了DelayQueue的底层源码实现,包括延迟出队的原理,以及入队列、出队列等操作的源码。

1 DelayQueue的概述

public class DelayQueue< E extends Delayed > extends AbstractQueue< E > implements BlockingQueue< E >

DelayQueue来自于JDK1.5的JUC包,是一个支持并发操作的无界阻塞队列,其最大的特点是支持延时出队操作,即队列的数据按照设定等待时间长短排序,队列头元素是剩余等待时间最短的元素!

队列中的元素类型必须实现Delayed接口,实际上Delayed就是Comparable接口的子接口,在存入元素的时候元素时可以指定延迟时间,通过getDelay获取剩余延迟时间,只有剩余延迟时间为0才能从队列头中获取元素。

DelayQueue底层物理结构直接采用了一个数组,数组可以动态扩容,逻辑结构则是实现了一个小顶堆的排序树,采用固定compareTo方法的返回值排序,基于堆的特点,不能保证同优先级元素的顺序,实际上内部仅仅维护了一个偏序关系。

没有实现Serializable和Cloneable接口,不支持序列化和克隆!

不支持null元素!

看起来和之前的PriorityBlockingQueue非常相似,事实上DelayQueue就可以看成一个把时间作为比较的key的优先级阻塞队列!因此,关于小顶堆的原理在本文不会讲解,在PriorityBlockingQueue文章中已经详细讲解了,如果我们明白PriorityBlockingQueue的原理,那么DelayQueue的原理还是很简单的!JUC—两万字的PriorityBlockingQueue源码深度解析

2 DelayQueue的原理

2.1 主要属性

DelayQueue内部使用一个PriorityQueue对象来存放数据,这个PriorityQueue就是用于构建小顶堆堆的数据结构以及排序,类似于PriorityBlockingQueue,但是这里的阻塞和唤醒都是DelayQueue控制的,因此使用PriorityQueue就行了。

PriorityQueue不是线程安全的,因此使用一个ReentrantLock锁对象来保证线程安全,生产和消费都需要获取同一个锁。只有一个条件变量available,用于消费线程的等待和唤醒,生产线程不会等待,因为队列是“无界”的,可以一直入队。

一个leader线程变量,在队列中存在元素的时候,第一个调用take()方法的线程将成为leader线程,它将会在available上等待队列头结点剩余的延迟时间,其他的线程将会成为follower线程,它们会一直在available上一直等待。leader线程苏醒之后会将leader变量置空,在获取到元素之后最后会唤醒一个在available上等待的follower线程。 被唤醒的follower线程将可能成为新的leader线程。

public class PriorityQueue<E> extends AbstractQueue<E>
        implements java.io.Serializable {

    /**
     * lock用于保证线程安全,生产和消费都需要获取同一个锁,创建对象实例的时候就初始化
     */
    private final transient ReentrantLock lock = new ReentrantLock();

    /**
     * 一个条件变量available,用于消费者线程的等待和唤醒,创建对象实例的时候就初始化
     * 生产线程不会等待,因为队列是“无界”的,可以一直入队。
     */
    private final Condition available = lock.newCondition();

    /**
     * 一个PriorityQueue实例作为真正存放元素的容器,内部是一个基于数组实现的小顶堆
     * 类似于PriorityBlockingQueue,创建对象实例的时候就初始化
     */
    private final PriorityQueue<E> q = new PriorityQueue<E>();

    /**
     * 在队列中存在元素的时候,第一个调用take()方法的线程将成为leader线程,它将会在available上等待队列头结点剩余的延迟时间
     * 其他的线程将会成为follower线程,它们会一直在available上一直等待
     * leader线程苏醒之后会将leader变量置空,在获取到元素之后最后会唤醒一个在available上等待的follower线程
     * 被唤醒的follower线程将可能成为新的leader线程
     */
    private Thread leader = null;
}
复制代码

存放的元素都是Delayed类型,Delayed又是Comparable接口的子接口,因此通过compareTo方法对存入的元素延迟时间进行比较大小从而排序,并且通过getDelay方法获取剩余延迟时间,剩余时间小于等于0即可出队。

/**
 * 用来标记那些应该在给定延迟时间之后执行的对象。
 * 此接口的实现必须定义一个 compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序。
 */
public interface Delayed extends Comparable<java.util.concurrent.Delayed> {

    /**
     * 返回与此对象相关的剩余延迟时间,以给定的时间单位表示。
     *
     * @param unit 时间单位
     * @return 返回与此对象相关的剩余延迟时间,以给定的时间单位表示。 0或者负数表示延迟完毕
     */
    long getDelay(TimeUnit unit);
}
复制代码

2.2 构造器

2.2.1 DelayQueue()

public DelayQueue()

创建一个最初为空的新 DelayQueue。

/**
 * 创建一个最初为空的新 DelayQueue。
 */
public DelayQueue() {
    //什么也不做
}
复制代码

2.2.2 DelayQueue(c)

public DelayQueue(Collection<? extends E> c)

创建一个最初包含 Delayed 实例的指定集合元素的 DelayQueue。实际上就是对指定集合c的元素循环调用add方法添加到此集合的过程。

如果指定集合或其任意元素为 null,则抛出NullPointerException。

/**
 * 创建一个最初包含 Delayed 实例的指定集合元素的 DelayQueue。
 *
 * @param c 要最初包含的元素的指定集合
 * @throws NullPointerException 如果指定集合或其任意元素为 null
 */
public DelayQueue(Collection<? extends E> c) {
    //直接调用了AbstractQueue的公共addAll方法
    this.addAll(c);
}


/**
 * AbstractQueue的公共addAll方法
 * 将指定集合中的所有元素都添加到此集合中。
 *
 * @param c 包含要添加到此集合的元素的指定集合
 * @return 如果此集合由于调用而发生更改,则返回 true
 * @throws ClassCastException       如果指定集合中某个元素的类不允许它添加到此集合中
 * @throws NullPointerException     如果指定集合包含 null 元素,并且此集合不支持 null 元素,或者指定集合为 null
 * @throws IllegalArgumentException 如果指定集合的元素的某属性不允许它添加到此集合中
 * @throws IllegalStateException    如果由于插入限制,不是所有的元素都能在此时间添加
 */
public boolean addAll(Collection<? extends E> c) {
    //如果c为null,抛出NullPointerException
    if (c == null)
        throw new NullPointerException();
    //如果自己添加自己,抛出IllegalArgumentException
    if (c == this)
        throw new IllegalArgumentException();
    //此集合结构改变的标志位,false表示此集合结构没有更改,true表示已更改
    boolean modified = false;
    /*循环指定集合,调用add方法*/
    for (E e : c)
        if (add(e))
            //add添加成功,那么modified置为true
            modified = true;
    //返回modified
    return modified;
}
复制代码

2.3 入队操作

由于是无界队列,因此入队操作不会因为队列满了而被阻塞,但是如果在容量/内存资源被耗尽时试图执行入队操作也将失败(导致OutOfMemoryError)。

2.3.1 offer(e)方法

public boolean offer(E e)

将指定元素插入此延迟队列。该队列是无界的,所以此方法不会阻塞,一定会返回true。

这里的“不会阻塞”是说的获取锁之后不会检查队列是否已满,一定会添加结点成功!因此如果该锁被其他线程获取了,当前调用offer方法的线程还是会因为获取不到锁而被阻塞在lock的同步队列中!

如果指定元素为 null,那么抛出NullPointerException。

大概步骤就是:

  1. 不可中断的等待获取lock锁,即不响应中断,没有获取到锁就阻塞,获取到锁之后,才进行下面的步骤;
  2. 调用q.offer方法,非阻塞的将元素加入到PriorityQueue中,并根据compareTo方法比较并构建小顶堆,小顶堆的详细原理在PriorityBlockingQueue的文章中已经详细讲解了。compareTo方法应该根据延迟时间比较大小,这样才能满足DelayQueue的性质;
  3. 如果调用peek方法获取的队列头部元素就是e,这表示新加入的元素e就是延迟时间最短的元素。无论此前有没有leader,此时都可以选出新的leader,因为有了延迟更短的元素:
    1. 那么leader置为null,让后来的线程可以当选为leader;
    2. 唤醒一个在available上等待的消费线程,让它和新消费线程重新争夺leader。
  4. 如果上面没有抛出异常,那么返回true;无论上面有没有抛出异常,最终都会解锁;
/**
 * 将指定元素插入此延迟队列。
 *
 * @param e 指定元素
 * @return 该队列是无界的,所以此方法不会阻塞,一定会返回true。
 * @throws NullPointerException 如果指定元素为 null
 */
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    //不可中断的等待获取lock锁,即不响应中断
    lock.lock();
    try {
        //调用q.offer方法,非阻塞的将元素加入到PriorityQueue中,并根据compareTo方法比较并构建小顶堆
        //小顶堆的详细原理在PriorityBlockingQueue的文章中已经详细讲解了
        //compareTo方法应该根据延迟时间比较大小,这样才能满足DelayQueue的性质
        q.offer(e);
        //如果调用peek()方法获取的队列头部元素就是e,这表示新加入的元素e就是延迟时间最短的元素
        //无论此前有没有leader,此时都可以选出新的leader,因为有了延迟更短的元素
        if (q.peek() == e) {
            //那么leader置为null
            leader = null;
            //唤醒一个在available上等待的消费线程,让它和新消费线程重新争夺leader
            available.signal();
        }
        //如果上面没有抛出异常,那么返回true
        return true;
    } finally {
        //无论上面有没有抛出异常,最终都会解锁
        lock.unlock();
    }
}
复制代码

2.3.2 add(e)方法

public boolean add(E e)

将指定元素插入此延迟队列。该队列是无界的,所以此方法不会阻塞,一定会返回true。

这里的“不会阻塞”是说的获取锁之后不会检查队列是否已满,一定会添加结点成功!因此如果该锁被其他线程获取了,当前调用offer方法的线程还是会因为获取不到锁而被阻塞在lock的同步队列中!

如果指定元素为 null,那么抛出NullPointerException。

内部就是调用的offer(e)方法!

/**
 * 将指定元素插入此延迟队列。
 *
 * @param e 指定元素
 * @return 该队列是无界的,所以此方法不会阻塞,一定会返回true。
 * @throws NullPointerException 如果指定元素为 null
 */
public boolean add(E e) {
    //内部就是调用的offer(e)方法
    return offer(e);
}
复制代码

2.3.3 put(e)方法

public void put(E e)

将指定元素插入此延迟队列。该队列是无界的,所以此方法不会阻塞。

这里的“不会阻塞”是说的获取锁之后不会检查队列是否已满,一定会添加结点成功!因此如果该锁被其他线程获取了,当前调用put方法的线程还是会因为获取不到锁而被阻塞在lock的同步队列中!

如果指定元素为 null,那么抛出NullPointerException。

内部实际上就是调用的offer(e)方法!

/**
 * 将指定元素插入此延迟队列。该队列是无界的,所以此方法不会阻塞。
 *
 * @param e 指定元素
 * @throws NullPointerException 如果指定元素为 null
 */
public void put(E e) {
    //内部就是调用的offer(e)方法
    offer(e);
}
复制代码

2.3.4 offer(e, timeout, unit)方法

public boolean offer(E e, long timeout, TimeUnit unit)

将指定元素插入此优先级队列。该队列是无界的,所以此方法不会阻塞,应该忽略timeout 和unit参数,这个方法只为了兼容父接口BlockingQueue的同名抽象方法!

这里的“不会阻塞”是说的获取锁之后不会检查队列是否已满,一定会添加结点成功!因此如果该锁被其他线程获取了,当前调用put方法的线程还是会因为获取不到锁而被阻塞在lock的同步队列中!

如果指定元素为 null,那么抛出NullPointerException。

内部实际上就是调用的offer(e)方法!

/**
 * 将指定元素插入此延迟队列。该队列是无界的,所以此方法不会阻塞,应该忽略timeout 和unit参数。
 *
 * @param e       指定元素
 * @param timeout 忽略此参数,因为此方法不会阻塞
 * @param unit    忽略此参数,因为此方法不会阻塞
 * @return true
 * @throws NullPointerException 如果指定元素为 null
 */
public boolean offer(E e, long timeout, TimeUnit unit) {
    //内部就是调用的offer(e)方法
    return offer(e);
}
复制代码

2.4 出队操作

虽然是无界队列,但是某些方法在出队的时候仍然需要判断队列是否为null,如果为null将可能被阻塞。

2.4.1 take()方法

public E take()

获取并移除此延迟队列已过期的队头,如果此时没有已过期的队头,那么一直等待。

如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。

大概步骤为:

  1. 可中断的等待获取lock锁,即响应中断;没有获取锁则阻塞在同步队列中,如果被中断则抛出异常并返回;获取锁之后进入下一步;
  2. 进入try块中,开启一个死循环,获取并移除队头元素,即延迟时间最短的元素或者延迟完毕的元素:
    1. 调用peek方法获取但不移除队列头部元素first;
    2. 如果队列头元素为null,说明队列为空,只能等待:
      1. 当前线程在available上无限等待,成为follower线程,直到被唤醒或被中断,随后继续下一次循环。
    3. 否则,说明队列非空,可以尝试让队头出队:
      1. 获取队列头部元素first的剩余延迟时间delay纳秒,如果delay小于等于0,说明队头可以出队列,调用q.poll()方法移除并返回队列头部元素,并重构小顶堆,这是出队列操作的唯一正常出口。
      2. 到这里,说明delay大于0,即还没有到时间,那么将该线程方法栈中的first保留的队头元素引用置空,因为接下来该线程也将会进入等待,等待完毕之后会重新循环获取队头。
      3. 如果leader线程不为null,说明:有线程成为了leader线程在进行超时等待了,并且没有新元素入队或者有新元素但是新元素不是延迟时间最短的元素。上面的情况说明有一个leader线程在等待延迟时间最短的元素出队列,那么该线程自然成为follower线程。在available上无限等待,直到被唤醒或被中断,随后继续下一次循环。
      4. 否则,如果leader线程为null,说明说明:目前没有线程成为了leader线程,可能是前一个leader线程被唤醒了,或者原本是有leader线程在进行超时等待,但是后来新入队列的元素延迟时间最短,成为了队头,此时leader引用被入队操作置空。满足上面的情况,那么此时的消费线程成为leader线程:
        1. leader赋值为当前线程thisThread;
        2. 当前线程thisThread在available上超时等待delay纳秒,直到被唤醒或被中断或超时时间到了自然唤醒。当thisThread正常返回之后,理想情况就是此时目前的队列头部元素first的剩余延迟时间也同时为0,那么该线程进入下一次循环,将会使队头出队;
        3. 超时等待过程中,无论被唤醒或被中断或超时时间到了自然唤醒,都会执行finally:如果此时的leader还是指向当前线程,那么leader置空,后续循环有可能重新成为leader线程,也可能成为follower线程。
  3. 进入try中之后的线程,无论是是正常执行完毕,还是抛出异常,都会执行该finally:
    1. 如果leader为null,并且此时队列不为空。唤醒一个在available上等待的消费线程,让它和新消费线程重新争夺leader。
    2. 解锁。
    /**
     * 获取并移除此队列的头部,在可从此队列获得到期延迟的元素之前(队列非空)一直等待。
     *
     * @return 队列头元素
     * @throws InterruptedException 如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,
     *                              如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。
     */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        //可中断的等待获取lock锁,即响应中断
        lock.lockInterruptibly();
        try {
            /*开启一个死循环,获取并移除队头元素,即延迟时间最短的元素或者延迟完毕的元素*/
            for (; ; ) {
                //获取但不移除队列头部元素first
                E first = q.peek();
                /*如果队列头元素为null,说明队列为空*/
                if (first == null)
                    //当前线程在available上无限等待,成为follower线程,直到被唤醒或被中断
                    available.await();
                    /*否则,说明队列非空*/
                else {
                    //获取队列头部元素first的剩余延迟时间delay纳秒
                    long delay = first.getDelay(NANOSECONDS);
                    /*如果delay小于等于0,说明队头可以出队列*/
                    if (delay <= 0)
                        //调用q.poll()方法移除并返回队列头部元素,并重构小顶堆,这是出队列操作的唯一正常出口。
                        return q.poll();
                    /*
                     * 到这里,说明delay大于0,即还没有到时间,那么将该线程方法栈中的first保留的队列头部不元素引用置空
                     * 因为接下来该线程也将会进入等待,等待完毕之后会重新循环获取队列头部
                     * 
                     * 如果不置空,可能造成内存泄漏,因为first指向的头结点可能被多个线程内部的first变量同时持有,如果在线程等待之前不将first置空,
                     * 那么如果某个时刻该结点出队列,那么这个结点理应被清除,但是由于还有很多等待的线程在持有该结点对象的引用,
                     * 导致该结点对象不能被即时的清理,造成内存泄漏
                     */
                    first = null; // don't retain ref while waiting
                    /*
                     * 如果leader线程不为null,说明:
                     * 有线程成为了leader线程在进行超时等待了,并且 没有新元素入队,或者有新元素但是新元素不是延迟时间最短的元素
                     * 上面的情况说明,有一个leader线程在等待延迟时间最短的元素出队列,那么该线程自然成为follower线程
                     */
                    if (leader != null)
                        //在available上无限等待,直到被唤醒或被中断,随后继续下一次循环。
                        available.await();
                        /*
                         * 如果leader线程为null,说明说明:
                         * 1 目前没有线程成为了leader线程,可能是前一个leader线程被唤醒了
                         * 2 原本是有leader线程在进行超时等待,但是后来新入队列的元素延迟时间最短,成为了队头,此时leader引用被入队操作置空
                         *
                         * 满足上面的情况,那么此时的消费线程成为leader线程
                         */
                    else {
                        //获取当前线程thisThread
                        Thread thisThread = Thread.currentThread();
                        //leader赋值为thisThread
                        leader = thisThread;
                        try {
                            //当前线程thisThread在available上超时等待delay纳秒,直到被唤醒或被中断或超时时间到了自然唤醒
                            //当thisThread正常返回之后,理想情况就是此时目前的队列头部元素first的剩余延迟时间也同时为0
                            //那么该线程进入下一次循环,将会使队头出队
                            available.awaitNanos(delay);
                        } finally {
                            //无论是awaitNanos自然唤醒,还是被中断唤醒,都会执行finally
                            //如果此时的leader还是指向当前线程
                            if (leader == thisThread)
                                //那么leader置空,后续循环有可能重新成为leader线程,也可能成为follower线程
                                leader = null;
                        }
                    }
                }
            }
        }
        //进入try中之后的线程,无论是是正常执行完毕,还是抛出异常,都会执行该finally
        finally {
            //如果leader为null 并且 此时队列不为空
            if (leader == null && q.peek() != null)
                //唤醒一个在available上等待的消费线程,让它和新消费线程重新争夺leader
                available.signal();
            //解锁
            lock.unlock();
        }
    }
复制代码

2.4.2 poll()方法

public E poll()

获取并移除此延迟队列已过期的队头,如果此时没有过期的队头,那么直接返回null。

相比于take方法,如果因为获取不到锁而在同步队列中等待的时候被中断也会继续等待获取锁,即不响应中断。

/**
 * 获取并移除此延迟队列已过期的头,如果此时队列没有过期的元素,那么直接返回null。
 *
 * @return 此队列的头部,或者如果此时队列没有具有过期延迟的元素,那么返回null。
 */
public E poll() {
    final ReentrantLock lock = this.lock;
    //不可中断的等待获取lock锁,即不响应中断
    lock.lock();
    try {
        //获取但不移除队列头部元素first
        E first = q.peek();
        /*如果队列为空,或者队头元素的剩余延迟时间大于0,那么直接返回null*/
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
            /*否则,说明队列不为空并且此时队头元素已经过期,那么调用q.poll获取并移除队头*/
        else
            return q.poll();
    } finally {
        //释放锁
        lock.unlock();
    }
}
复制代码

2.4.3 poll(timeout, unit)方法

public E poll(long timeout, TimeUnit unit)

在指定时间内等待获得到期的队列头;如果在队头过期之前超过了指定的等待时间,则返回 null。

如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。

它的源码和take方法非常相似,只不过增加了等待的超时时间而已!

/**
 * @return 在指定时间内等待获得到期的队列头;如果在队头过期之前超过了指定的等待时间,则返回 null。
 * @throws InterruptedException 如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,
 *                              如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。
 */
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    //计算剩余超时时间纳秒
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    //可中断的等待获取lock锁,即响应中断
    lock.lockInterruptibly();
    try {
        /*开启一个死循环,获取并移除队头元素,即延迟时间最短的元素或者延迟完毕的元素*/
        for (; ; ) {
            //获取但不移除队列头部元素first
            E first = q.peek();
            /*如果队列头元素为null,说明队列为空*/
            if (first == null) {
                /*继续判断如果超时时间小于0,说明超过了指定时间还没有移除*/
                if (nanos <= 0)
                    //直接返回null
                    return null;
                    /*否则,当前线程在available上等待nanos,成为follower线程,直到被唤醒或被中断或超时时间到了自然唤醒 */
                else
                    nanos = available.awaitNanos(nanos);
            }
            /*否则,说明队列非空*/
            else {
                //获取队列头部元素first的剩余延迟时间delay纳秒
                long delay = first.getDelay(NANOSECONDS);
                /*如果delay小于等于0,说明队头可以出队列*/
                if (delay <= 0)
                    //调用q.poll()方法移除并返回队列头部元素,并重构小顶堆,这是出队列操作的唯一正常出口。
                    return q.poll();
                /*
                 * 到这里,说明delay大于0,即还没有到时间,那么将该线程方法栈中的first保留的队列头部不元素引用置空
                 * 因为接下来该线程也将会根据剩余超时时间继续等待,或者返回null
                 * 继续判断如果超时时间小于0,说明超过了指定时间还没有移除
                 */
                if (nanos <= 0)
                    //直接返回null
                    return null;
                /*
                 * 到这里,说明delay大于0,即还没有过期,同时超时时间也没有过期,那么将该线程方法栈中的first保留的队列头部不元素引用置空
                 * 因为接下来该线程也将会进入等待,等待完毕之后会重新循环获取队列头部
                 */
                first = null; // don't retain ref while waiting
                /*
                 *
                 * 如果线程超时时间nanos小于队头的延迟时间delay
                 * 否则,如果leader线程不为null,说明:
                 *      有线程成为了leader线程在进行超时等待了,并且 没有新元素入队,或者有新元素但是新元素不是延迟时间最短的元素
                 *      上面的情况说明,有一个leader线程在等待延迟时间最短的元素出队列,那么该线程自然成为follower线程
                 *
                 * 以上两种情况,都需要需要等待nanos
                 */
                if (nanos < delay || leader != null)
                    //在available上等待超时时间,直到被唤醒或被中断,随后继续下一次循环,下一次循环无论如何都会返回
                    nanos = available.awaitNanos(nanos);
                    /*
                     * 如果线程超时时间nanos大于等于队头的延迟时间delay,并且如果leader线程为null,说明说明:
                     * 1 目前没有线程成为了leader线程,可能是前一个leader线程被唤醒了
                     * 2 原本是有leader线程在进行超时等待,但是后来新入队列的元素延迟时间最短,成为了队头,此时leader引用被入队操作置空
                     *
                     * 满足上面的情况,那么此时的消费线程成为leader线程,并且等待delay
                     */
                else {
                    //获取当前线程thisThread
                    Thread thisThread = Thread.currentThread();
                    //leader赋值为thisThread
                    leader = thisThread;
                    try {
                        //当前线程thisThread在available上超时等待delay纳秒,直到被唤醒或被中断或超时时间到了自然唤醒
                        //当thisThread正常返回之后,理想情况就是此时目前的队列头部元素first的剩余延迟时间也同时为0,那么该线程进入下一次循环,将会使队头出队
                        long timeLeft = available.awaitNanos(delay);
                        //但是也有可能因为中断而返回或者中途被唤醒,此时需要更新剩余等待时间,下一次循环中,如果没有能够出队,并且剩余等待时间大于0,那么继续等待
                        nanos -= delay - timeLeft;
                    } finally {
                        //无论是awaitNanos自然唤醒,还是被中断唤醒,都会执行finally
                        //如果此时的leader还是指向当前线程
                        if (leader == thisThread)
                            //那么leader置空,后续循环有可能重新成为leader线程,也可能成为follower线程
                            leader = null;
                    }
                }
            }
        }
    }
    //进入try中之后的线程,无论是是正常执行完毕,还是抛出异常,都会执行该finally
    finally {
        //如果leader为null 并且 此时队列不为空
        if (leader == null && q.peek() != null)
            //唤醒一个在available上等待的消费线程,让它和新消费线程重新争夺leader
            available.signal();
        //解锁
        lock.unlock();
    }
}
复制代码

2.4.4 remove()方法

public E remove()

获取并移除此延迟队列已过期的队头。此方法与 poll 唯一的不同在于如果此时没有过期的队头将抛出一个NoSuchElementException异常。

相比于take方法,如果因为获取不到锁而在同步队列中等待的时候被中断也会继续等待获取锁,即不响应中断。

内部实际上就是调用的poll方法,根据poll方法的返回值判断是否需要抛出异常!

/**
 * @return 获取并移除此延迟队列已过期的队头。此方法与 poll 唯一的不同在于如果此时没有过期的队头将抛出一个NoSuchElementException异常。
 * @throws NoSuchElementException 此队列为空
 */
public E remove() {
    //直接调用poll方法,获取返回值x
    E x = poll();
    //如果x不为null,那么返回x;否则抛出NoSuchElementException异常
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}
复制代码

2.4.5 remove(o)方法

public boolean remove(Object o)

如果存在,则从此队列中移除指定元素的单个实例,无论是否过期。如果移除成功则返回 true;没有找到指定元素或者指定元素为null则返回false。

/**
 * 如果存在,则从此队列中移除指定元素的单个实例,无论是否过期。
 *
 * @param o 指定元素
 * @return 如果移除成功则返回 true;没有找到指定元素或者指定元素为null则返回false。
 */
public boolean remove(Object o) {
    final ReentrantLock lock = this.lock;
    //不可中断的等待获取lock锁,即不响应中断
    lock.lock();
    try {
        //调用q.remove方法从小顶堆移除找到的第一个和o相等的元素,并重构小顶堆,返回poll的返回值
        return q.remove(o);
    } finally {
        //解锁
        lock.unlock();
    }
}
复制代码

2.5 检查操作

2.5.1 peek()方法

public E peek()

获取但不移除此队列的头(无论是否过期);如果此队列为空,则返回 null。

/**
 * @return 获取但不移除此队列的头(无论是否过期);如果此队列为空,则返回 null。
 */
public E peek() {
    final ReentrantLock lock = this.lock;
    //不可中断的等待获取lock锁,即不响应中断
    lock.lock();
    try {
        //调用q.peek方法获取队列头,即小顶堆的根元素,如果队列为null则返回null
        return q.peek();
    } finally {
        //解锁
        lock.unlock();
    }
}
复制代码

2.5.2 element()方法

public E element()

获取但是不移除此队列的头(无论是否过期)。此方法与 peek 唯一的不同在于此队列为空时将抛出一个异常。

/**
 * 获取但是不移除此队列的头(无论是否过期)。此方法与 peek 唯一的不同在于此队列为空时将抛出一个异常。
 *
 * @return 队头
 * @throws NoSuchElementException 如果此队列为空
 */
public E element() {
    //内部调用peek方法获取返回值x
    E x = peek();
    //如果x不为null,那么返回x;否则抛出NoSuchElementException异常
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}
复制代码

2.6 size操作

public int size()

返回此队列中元素的数量。这个方法加了锁,因此返回的是精确值!

public boolean isEmpty()

如果此队列不包含元素,则返回 true。

public int size() {
    final ReentrantLock lock = this.lock;
    //不可中断的等待获取lock锁,即不响应中断
    lock.lock();
    try {
        //调用q.size()
        return q.size();
    } finally {
        //释放锁
        lock.unlock();
    }
}

/**
 * 直接判断size()方法是否返回0
 */
public boolean isEmpty() {
    return size() == 0;
}
复制代码

2.7 迭代操作

public Iterator< E > iterator()

返回在队列中的元素上按适当顺序进行迭代的迭代器Iterator。和其他并发容器一样,返回的 Iterator 是一个“弱一致”的,不会抛出 ConcurrentModificationException,即支持并发修改,但是不保证迭代获取的元素就是此时队列中的元素!

下面的源码解析包括了迭代器的实现和方法,还是很简单的。弱一致性的原理在注释中也讲的很清楚,实际上需要迭代的底层数组副本在创建迭代器实例时就被存储了起来,后续就是对这个副本数组进行迭代,自然和原来的数组没有了太大关系,但是数组里面的元素只是浅克隆而已,因此迭代器的remove方法使用==比较才可能实现!

toArray、remove等方法调用时都需要加锁,因此会影响并发效率!

/**
 * @return 返回在队列中的元素上按适当顺序进行迭代的迭代器Iterator(既包括到期的元素,也包括未到期的的元素)。
 * 和其他并发容器比如PriorityBlockingQueue一样,返回的 Iterator 是一个“弱一致”的,不会抛出 ConcurrentModificationException,
 * 即支持并发修改,但是不保证迭代获取的元素就是此时队列中的元素!
 */
public Iterator<E> iterator() {
    //返回一个Itr对象,传入toArray方法的返回值
    return new Itr(toArray());
}

/**
 * 返回包含此队列所有元素的数组。所返回数组的元素没有特定的顺序。
 *
 * @return 包含此队列所有元素的数组,相当于与元素的浅克隆
 */
public Object[] toArray() {
    final ReentrantLock lock = this.lock;
    //不可中断的等待获取lock锁,即不响应中断
    lock.lock();
    try {
        //调用q.toArray获取元素数组
        return q.toArray();
    } finally {
        //解锁
        lock.unlock();
    }
}


/**
 * “弱一致”的迭代器的内部实现
 */
private class Itr implements Iterator<E> {
    //存储的数组快照
    final Object[] array; // Array of all elements
    //要迭代的下一个元素的索引,初始为0
    int cursor;           // index of next element to return
    //最后一次迭代的元素的索引,初始为-1,没有也是-1,用于辅助删除
    int lastRet;          // index of last element, or -1 if no such

    /**
     * 构造器
     *
     * @param array 底层数组副本
     */
    Itr(Object[] array) {
        //lastRet置为-1
        lastRet = -1;
        //array赋值
        this.array = array;
    }

    /**
     * 是否有下一个结点
     *
     * @return true 是 false 否
     */
    public boolean hasNext() {
        //返回cursor是否小于快照数组的长度
        return cursor < array.length;
    }

    /**
     * 返回下一个结点的值
     * lastRet等于此时的current,同时计算下一个要返回的结点current和下一个要返回的值currentElement
     *
     * @return 下一个结点的值
     */
    @SuppressWarnings("unchecked")
    public E next() {
        //如果current大于等于快照数组的长度,表示已经没有下一个结点了,直接抛出NoSuchElementException异常
        if (cursor >= array.length)
            throw new NoSuchElementException();
        //lastRet等于此时的cursor,即最后一次迭代的元素索引
        lastRet = cursor;
        //返回底层数组cursor索引的值,然后cursor自增1
        return (E) array[cursor++];
    }

    /**
     * 移除上一次next方法返回的元素,即最后一次迭代的结点
     */
    public void remove() {
        //如果lastRet 小于0,表示最后迭代的结点已经被移除了,或者还没有开始迭代,直接抛出IllegalStateException异常
        if (lastRet < 0)
            throw new IllegalStateException();
        //调用removeEQ尝试在真正的底层数组queue中移除和array[lastRet]元素相等的找到的第一个元素
        removeEQ(array[lastRet]);
        //lastRet重置为-1,表示最后迭代的结点已经被移除了
        lastRet = -1;
    }
}

/**
 * 位于DelayQueue 中的方法
 * 用于Itr.remove()方法调用,移除底层数组中找到的与指定元素o相等的第一个元素
 * 使用 == 比较是否相等
 *
 * @param o 指定需要被移除的元素
 */
void removeEQ(Object o) {
    final ReentrantLock lock = this.lock;
    //不可中断的等待获取lock锁,即不响应中断
    lock.lock();
    try {
        //获取底层q的迭代器it,遍历该迭代器,注意这个迭代器是fail-fast,会抛出ConcurrentModificationException,
        //因此需要加锁保证线程安全,在迭代过程中只能是单线程操作
        for (Iterator<E> it = q.iterator(); it.hasNext(); ) {
            //这里和remove(o)方法不一样,这里是使用 == 比较的,而remove(o)
            //如果o等于目前的q的迭代器的某个位置的元素,这里的相等就是指的同一个对象
            if (o == it.next()) {
                //调用迭代器的remove方法,通过迭代器从q的底层数组中移除该元素
                it.remove();
                //跳出循环
                break;
            }
        }
    } finally {
        //解锁
        lock.unlock();
    }
}
复制代码

3 DelayQueue的应用

由于DelayQueue 的延时特性,常被用于定时缓存清除,以及定时任务调度。入队列的任务将按照开始时间构建小顶堆,队列头的任务就是最近最快要开始的任务,然后可以使用一个消费者线程,不停的从DelayQueue中take获取任务然后执行。

Java中的Timer定时器中的TimerQueue任务队列就是基于DelayQueue的封装,存放了所有的TimerTask。

3.1 案例

首先我们需要一个实现Delayed接口的元素类DelayedEle,delayTime属性表示当前元素需要延迟多少ms 之后过期,即延迟时间;expire则是当前时间的ms值加上delayTime 的值,即到期时间点,taskName表示任务名称。getDelay方法用来获取当前元素还剩下多少时间过期,compareTo方法用于在存入队列的时候来决定在小顶堆中的顺序,即最初始的任务偏序排序。

在main 函数内首先创建了一个延迟队列,然后使用随机数生成器生成了10 个延迟任务,最后通过循环依次获取延迟任务,并打印。

/**
 * @author lx
 */
public class DelayQueueTest {
    static class DelayedEle implements Delayed {
        private final long delayTime; //延迟时间
        private final long expire; //到期时间
        private String taskName; //延迟时间


        /**
         * 构造器
         *
         * @param delayTime 延迟时间,毫秒
         * @param taskName  任务名称
         */
        DelayedEle(long delayTime, String taskName) {
            this.delayTime = delayTime;
            //计算出到期时间点 -> 延迟时间+ 当前时间
            this.expire = delayTime + System.currentTimeMillis();
            this.taskName = taskName;
        }

        /**
         * 在调用DelayQueue的出队列的时候会调用该方法并传递参数 NANOSECONDS
         *
         * @param unit 时间单位,
         * @return 返回剩余延迟时间纳秒 -> 到期时间-当前时间
         */
        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        /**
         * 用于加入队列时元素之间比较构建小顶堆的方法
         *
         * @param o 参数对象
         * @return 如果返回负数,表示this对象小于,那么this对象将比参数对象优先出队列
         */
        @Override
        public int compareTo(Delayed o) {
            //比较this对象和参数对象剩余时间长短
            //this对象剩余时间大于参数对象剩余时间,则返回大于0。那么将会设置this对象的出队列优先级小于当前对象
            //this对象剩余时间小于参数对象剩余时间,则返回小于0。那么将会设置this对象的出队列优先级大于当前对象
            return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
        }

        @Override
        public String toString() {
            return "DelayedEle{" +
                    "delayTime=" + delayTime +
                    ", expire=" + expire +
                    ", taskName='" + taskName + '\'' +
                    '}';
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //创建DelayQueue队列
        DelayQueue<DelayedEle> delayQueue = new DelayQueue<>();
        //创建并添加10个任务
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            //每个任务的延迟时间在[0,1000)之间
            DelayedEle delayedEle = new DelayedEle(random.nextInt(1000), "task " + (i + 1));
            delayQueue.add(delayedEle);
        }
        //循环获取过期任务并打印,可以看到结果就是延迟时间最短的任务最先出队,这和任务添加的顺序无关
        while (true) {
            System.out.println(delayQueue.take());
            if (delayQueue.isEmpty()) {
                break;
            }
        }
    }
}
复制代码

4 DelayQueue的总结

DelayQueue是一个支持延时出队的队列,元素类型必须是Delayed类型,主要是为了入队时通过compareTo方法比较延时长短,以及后续出队列操作时通过getDelay方法获取剩余延时时间。

消费线程在等待元素过期的时候,会有一个线程成为leader线程,用于表示该线程在等待队头元素,他会进行限时的等待,等待时间就是此时队头元素的剩余延迟时间。其他的线程如果发现leader不为空,则会一直等待,成为follower线程。当leader线程被唤醒之后会将leader置空,其他线程可以竞争leader,并且每一个不是立即返回出队列方法在返回的时候,如果leader为null并且队列不为空,那么会唤醒一个睡眠的follower线程。leader-follower模式可以避免没有必要的自旋或者没必要的唤醒,同时每一个leader的释放代表着一个队头元素的出队列操作,随后会另一个元素会成为队头,同时另一个线程将会成为新leader等待新的队头。

DelayQueue的源码还是非常简单的,实际上就是对PriorityQueue的包装,类似于装饰设计模式,元素存储在内部的PriorityQueue对象中,各种方法都是基于PriorityQueue的方法,DelayQueue则是实现了线程的阻塞、唤醒以及线程安全。元素的优先级按照剩余延迟时间来排列的,底层是一个小顶堆结构,队列头部元素就是剩余延迟时间最小的元素。所以如果明白PriorityBlockingQueue的原理那么DelayQueue的原理还是非常简单的。

相关文章:

  1. PriorityBlockingQueue:JUC—两万字的PriorityBlockingQueue源码深度解析
  2. 堆排序:10种常见排序算法原理详解以及Java代码的完全实现,其中有关于堆排序的详解。
  3. 堆结构:数据结构—堆(Heap)的原理介绍以及Java代码的完全实现,其中有关于大顶堆和小顶堆的介绍和构建实现!
  4. ReentrantLock:JUC—ReentrantLock源码深度解析

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改