Java 并发 --- 阻塞队列之DelayQueue源码分析

来源:互联网 发布:王励勤多娜 知乎 编辑:程序博客网 时间:2024/05/18 15:07

DelayQueue 介绍(jdk 1.8)

DelayQueue 是一个支持延时获取元素的无界阻塞队列,队列使用PriorityQueue来实现,队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素,只有在延时期满时才能从队列中提取元素。
PriorityQueue 是一种优先级的队列,队列中的元素会按照优先级进行排序,我们在前面讲过PriorityBlockingQueue,PriorityQueue其实现原理也是使用的二叉堆,因此这次将不会再分析PriorityQueue的原理了,可以参考Java 并发 — 阻塞队列之PriorityBlockingQueue 中关于对二叉堆的分析。

Delayed 接口分析

public interface Delayed extends Comparable<Delayed> {    /**     * Returns the remaining delay associated with this object, in the     * given time unit.     *     * @param unit the time unit     * @return the remaining delay; zero or negative values indicate     * that the delay has already elapsed     */    long getDelay(TimeUnit unit);}

Delayed 接口有一个getDelay 方法接口,该方法用来告知延迟到期有多长的时间,或者延迟在多长时间之前已经到期,是不是很简单。
为了排序Delayed 接口还继承了Comparable 接口,因此必须实现compareTo(),使其可以进行元素的比较。

DelayQueue 继承体系

这里写图片描述
和阻塞队列ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue基本是一样的,唯独没有实现序列化接口
DelayQueue 实现了BlockingQueue接口,该接口中定义了阻塞的方法接口,
DelayQueue 继承了AbstractQueue,具有了队列的行为。

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

DelayQueue 通过组合一个PriorityQueue 来实现元素的存储以及优先级维护,通过ReentrantLock 来保证线程安全,通过Condition 来判断是否可以取数据,对于leader我们后面再来分析它的作用。

构造方法

1、默认构造方法,这个简单,什么都没有

    public DelayQueue() {}

2、通过集合初始化

    public DelayQueue(Collection<? extends E> c) {        this.addAll(c);    }

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

入队

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);            //peek 是获取的队头元素,唤醒阻塞在available 条件上的一个线程,表示可以从队列中取数据了            if (q.peek() == e) {                leader = null;                available.signal();            }            return true;        } finally {            lock.unlock();        }    }

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

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

    /**     * Inserts the specified element into this delay queue. As the queue is     * unbounded this method will never block.     *     * @param e the element to add     * @param timeout This parameter is ignored as the method never blocks     * @param unit This parameter is ignored as the method never blocks     * @return {@code true}     * @throws NullPointerException {@inheritDoc}     */    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 来进行入队操作,入队超时方法并没有其超时功能。

出队

1、poll()

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

    /**     * Retrieves and removes the head of this queue, or returns {@code null}     * if this queue has no elements with an expired delay.     *     * @return the head of this queue, or {@code null} if this     *         queue has no elements with an expired delay     */    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);                    //延迟到期                    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。

4、peek()

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

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

总结

  1. DelayQueue 内部通过组合PriorityQueue 来实现存储和维护元素顺序的。
  2. DelayQueue 存储元素必须实现Delayed 接口,通过实现Delayed 接口,可以获取到元素延迟时间,以及可以比较元素大小(Delayed 继承Comparable)
  3. DelayQueue 通过一个可重入锁来控制元素的入队出队行为
  4. DelayQueue 中leader 标识 用于减少线程的竞争,表示当前有其它线程正在获取队头元素。
  5. PriorityQueue 只是负责存储数据以及为何元素的顺序,对于延迟时间取数据则是在DelayQueue 中进行判断控制的。
  6. DelayQueue 没有实现序列化接口

如果对优先级队列不清楚,可以参考一下这篇文章:
Java 并发 — 阻塞队列之PriorityBlockingQueue

原创粉丝点击