Java同步数据结构之DelayQueue/DelayedWorkQueue
前言
前面介绍了优先级队列PriorityBlockingQueue,顺带也说了一下PriorityQueue,两者的实现方式是一模一样的,都是采用基于数组的平衡二叉堆实现,不论入队的顺序怎么样,take、poll出队的节点都是按优先级排序的。但是PriorityBlockingQueue/PriorityQueue队列中的所有元素并不是在入队之后就已经全部按优先级排好序了,而是只保证head节点即队列的首个元素是当前最小或者说最高优先级的,其它节点的顺序并不保证是按优先级排序的,PriorityBlockingQueue/PriorityQueue队列只会在通过take、poll取走head之后才会再次决出新的最小或者说最高优先级的节点作为新的head,其它节点的顺序依然不保证。所以通过peek拿到的head节点就是当前队列中最高优先级的节点。
明白了优先级队列的原理要理解DelayQueue就非常简单,因为DelayQueue就是基于PriorityQueue实现的,DelayQueue队列实际上就是将队列元素保存到内部的一个PriorityQueue实例中的(所以也不支持插入null值),DelayQueue只专注于实现队列元素的延时出队。
延迟队列DelayQueue是一个无界阻塞队列,它的队列元素只能在该元素的延迟已经结束或者说过期才能被出队。它怎么判断一个元素的延迟是否结束呢,原来DelayQueue队列元素必须是实现了Delayed接口的实例,该接口有一个getDelay方法需要实现,延迟队列就是通过实时的调用元素的该方法来判断当前元素是否延迟已经结束。
既然DelayQueue是基于优先级队列来实现的,那肯定元素也要实现Comparable接口,没错因为Delayed接口继承了Comparable接口,所以实现Delayed的队列元素也必须要实现Comparable的compareTo方法。延迟队列就是以时间作为比较基准的优先级队列,这个时间即延迟时间,这个时间大都在构造元素的时候就已经设置好,随着程序的运行时间的推移,队列元素的延迟时间逐步到期,DelayQueue就能够基于延迟时间运用优先级队列并配合getDelay方法达到延迟队列中的元素在延迟结束时精准出队。
Delayed接口
1 public interface Delayed extends Comparable<Delayed> { 2 3 //以指定的时间单位unit返回此对象的剩余延迟 4 // 返回值 小于等于0 表示延迟已经结束 5 long getDelay(TimeUnit unit); 6 }
放入DelayQueue队列的元素必须实现Delayed接口,getDelay方法用于查看当前对象的延迟剩余时间,返回值是以参数指定的unit为单位的数字(unit可以是秒,分钟等),返回值小于等于0就表示该元素延迟结束。注意:该接口的实现类必须要实现compareTo方法,且compareTo方法提供与getDelay方法一致的排序,也就是说compareTo要基于getDelay方法的返回值来实现比较。
DelayQueue
现来看看它的成员变量:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class DelayQueue<E extends Delayed> extends AbstractQueue<E> 2 implements BlockingQueue<E> { 3 4 //非公平锁 5 private final transient ReentrantLock lock = new ReentrantLock(); 6 private final PriorityQueue<E> q = new PriorityQueue<E>(); //优先级队列 7 8 /* 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 9 * 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. 10 * The leader thread must signal some other thread before returning from take() or poll(...), unless some other thread becomes leader in the interim. 11 * 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, 12 * 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. 13 14 * 指定用于等待队列头部的元素的线程。这种Leader-Follower模式的变体(http://www.cs.wustl.edu/~schmidt/POSA/POSA2/)可以减少不必要的定时等待。 15 * 当一个线程成为leader时,它只等待下一个延迟过期,而其他线程则无限期地等待。leader线程必须在从take()或poll(…)返回之前向其他线程发出信号,除非其他线程在此期间成为leader。 16 * 每当队列的头部被具有更早过期时间的元素替换时,leader字段就会通过重置为null而无效,并且会通知等待的线程(不一定是当前的leader)的信号。 17 * 因此,等待线程必须准备好在等待时获得和失去领导权。 18 */ 19 private Thread leader = null; 20 21 /* Condition signalled when a newer element becomes available at the head of the queue or a new thread may need to become leader. 22 * 当队列头部的新元素可用或新线程可能需要成为leader时发出的条件。 23 */ 24 private final Condition available = lock.newCondition();
DelayQueue的成员很简单,一个非公平锁以及Condition、一个优先级队列,以及一个leader线程。这个leader线程很关键,它上面的Java Doc描述很长,其实就已经把DelayQueue的内部实现说的很清楚了。简单来说,DelayQueue使用了所谓的Leader-Follower模式的变体来减少消费线程不必要的定时等待,由于优先级队列中的head就是整个队列中最先要延迟结束的元素,其它元素的延迟结束时间都比它长,所以就让获取head元素的线程只等待它延时剩余的时间,而让其它消费线程无限期等待,当获取head元素的线程结束等待取走head之后再唤醒其它消费线程,那些消费线程又会因为拿到新的head而产生一个新的leader,这就是Leader-Follower模式,只等待指定时间的获取head元素的线程就叫leader,而其它所有调用take的消费线程都是Follower。当然如果在leader等待的过程中有延迟时间更短的元素入队,根据优先级队列的平衡二叉堆实现,它必然会被排到原来的head之前,成为新的head,这时候原来的leader线程可能就会失去leader的领导权,谁被非公平锁唤醒拿到新的head谁就是新的leader。
DelayQueue只有两个构造方法,一个无参,一个用指定的集合元素初始化延迟队列,源码很简单就不贴了。直接看DelayQueue的入队逻辑。
入队offer
DelayQueue的入队方法都是调用的offer实现,直接看offer就可以了:
1 public boolean offer(E e) { 2 final ReentrantLock lock = this.lock; 3 lock.lock(); 4 try { 5 q.offer(e); //直接PriorityQueue入队 6 if (q.peek() == e) { //如果当前是第一个入队的元素或者以前的head元素被当前元素替代了 7 leader = null; //剥夺leader的领导权,由于是非公平锁,唤醒的不一定是leader线程,所以需要重置为null 8 available.signal();//唤醒leader,更换head 9 } 10 return true; 11 } finally { 12 lock.unlock(); 13 } 14 }
入队操作很简单,直接调用的优先级队列的offer入队,如果是①第一个入队的元素或者是②当前时刻当前元素比原来的head具有更短的剩余延迟时间,那么是①的话需要唤醒因为队列空而阻塞的消费线程,是②的话需要剥夺当前leader的领导权,并随机唤醒(非公平锁)一个消费线程(如果有的话)成为新的leader。这里有一个问题,怎么就能判断当前入队的元素比head具有更短的延迟时间呢,因为head的延迟时间已经过去了一部分,其实这就需要在实现元素的compareTo方法时要根据getDelay方法返回的实时延迟剩余时间来作比较,这样优先级队列在通过siftUp冒泡确定新入队元素的位置的时候就能精确的把握实时的延迟剩余时间从而找到自己正确的位置。
出队take
DelayQueue的精髓就在出队方法的实现了,因为要保证只让延迟结束的元素才能出队:
1 /** 2 * Retrieves and removes the head of this queue, waiting if necessary 3 * until an element with an expired delay is available on this queue. 4 * 检索并删除此队列的头部,如果需要,将一直等待,直到该队列上具有延迟结束的元素为止。 5 * @return the head of this queue 6 * @throws InterruptedException {@inheritDoc} 7 */ 8 public E take() throws InterruptedException { 9 final ReentrantLock lock = this.lock; 10 lock.lockInterruptibly(); 11 try { 12 for (;;) { //注意是循环 13 E first = q.peek(); //获取但不移除head 14 if (first == null) //队列为空,等待生产者offer完唤醒 15 available.await(); 16 else { 17 long delay = first.getDelay(NANOSECONDS); 18 if (delay <= 0) //head元素已经过期 19 return q.poll(); 20 first = null; // don't retain ref while waiting 等待时不要持有引用 21 if (leader != null) //已经有leader在等待了,无限期等待leader醒来通知 22 available.await(); 23 else { 24 // 当前线程成为leader 25 Thread thisThread = Thread.currentThread(); 26 leader = thisThread; 27 try { 28 available.awaitNanos(delay); //等待heade元素剩余的延迟时间结束 29 } finally { 30 if (leader == thisThread) 31 leader = null; //交出leader权限 32 } 33 } 34 } 35 } 36 } finally { 37 if (leader == null && q.peek() != null) //队列不为空,并且没有leader 38 available.signal(); //唤醒其它可能等待的消费线程 39 lock.unlock(); 40 } 41 }
出队的逻辑就是Leader-Follower模式的实现,Leader只等待head剩余的延迟时间(28行),而Follower无限期等待(22行)Leader成功取走head之后来唤醒(finally的部分),如果由于offer入队更短剩余延迟时间的元素导致leader失去领导权,非公平锁唤醒的将可能是无限期等待的Follower,也可能是原来的Leader,由于offer重置了leader为null,所以被唤醒的线程能够立即拿走head返回(如果head已经延迟结束)或者重新成为leader(如果head还没有延迟结束)。
DelayQueue的其余方法例如立即返回的poll和指定超时时间的poll方法逻辑简单或者与take的实现原理一致就不作分析了,有个比较特殊的方法就是drainTo,它只会转移当前所有延迟已经结束的元素。peek方法返回的head不判断是否延迟结束只表示当前剩余延迟时间最少的元素,size方法返回队列中所有元素的个数包括延迟没有结束的。
DelayQueue的迭代器与PriorityBlockingQueue的迭代器实现一模一样,都是通过toArray将原队列数组元素拷贝到新的数组进行遍历,不会与原队列同步更新,但是remove可以删除原队列中的指定元素,而且迭代时不区分元素是否延迟结束。
DelayQueue的可拆分迭代器继承自Collection接口的默认迭代器实现IteratorSpliterator,和ArrayBlockingQueue一样,IteratorSpliterator的特性就是顶层迭代器实际上调用的是队列本身的Iterator迭代器实现,拆分后的迭代器按是数组下标方式的迭代,拆分按一半的原则进行,因此DelayQueue的可拆分迭代器也是脱离队列源数据的,不会随着队列变化同步更新。更多关于IteratorSpliterator的其它特性请到ArrayBlockingQueue章节中查看。
应用实例
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.Queue; 2 3 import java.util.concurrent.DelayQueue; 4 import java.util.concurrent.Delayed; 5 import java.util.concurrent.TimeUnit; 6 7 public class DelayQueueTest { 8 9 10 public static void main(String[] args) throws Exception { 11 DelayQueue<DelayTask> dq = new DelayQueue(); 12 13 //入队四个元素,注意它们的延迟时间单位不一样。 14 dq.offer(new DelayTask(5, TimeUnit.SECONDS)); 15 dq.offer(new DelayTask(2, TimeUnit.MINUTES)); 16 dq.offer(new DelayTask(700, TimeUnit.MILLISECONDS)); 17 dq.offer(new DelayTask(1000, TimeUnit.NANOSECONDS)); 18 19 while(dq.size() > 0){ 20 System.out.println(dq.take()); 21 } 22 23 /* 24 打印顺序: 25 DelayTask{delay=1000, unit=NANOSECONDS} 26 DelayTask{delay=700000000, unit=MILLISECONDS} 27 DelayTask{delay=5000000000, unit=SECONDS} 28 DelayTask{delay=120000000000, unit=MINUTES} 29 */ 30 } 31 } 32 33 class DelayTask implements Delayed { 34 35 private long delay; //延迟多少纳秒开始执行 36 private TimeUnit unit; 37 38 public DelayTask(long delay, TimeUnit unit){ 39 this.unit = unit; 40 this.delay = TimeUnit.NANOSECONDS.convert(delay, unit);//统一转换成纳秒计数 41 } 42 43 @Override 44 public long getDelay(TimeUnit unit) {//延迟剩余时间,单位unit指定 45 return unit.convert(delay - System.currentTimeMillis(), TimeUnit.NANOSECONDS); 46 } 47 48 @Override 49 public int compareTo(Delayed o) {//基于getDelay实时延迟剩余时间进行比较 50 if(this.getDelay(TimeUnit.NANOSECONDS) < o.getDelay(TimeUnit.NANOSECONDS)) //都换算成纳秒计算 51 return -1; 52 else if(this.getDelay(TimeUnit.NANOSECONDS) > o.getDelay(TimeUnit.NANOSECONDS)) //都换算成纳秒计算 53 return 1; 54 else 55 return 0; 56 } 57 58 @Override 59 public String toString() { 60 return "DelayTask{" + 61 "delay=" + delay + 62 ", unit=" + unit + 63 '}'; 64 } 65 }
假设有四个延迟任务,分别需要延迟不同的时间开始执行,上例中最终打印出的结果是按延迟剩余时间从小到大排列的。
DelayedWorkQueue
顺便把ScheduledThreadPoolExecutor的内部类DelayedWorkQueue也说一下把,它也是一种无界延迟阻塞队列,它主要用于线程池定时或周期任务的使用,关于线程池和定时任务在后面的章节介绍,本文仅限于分析DelayedWorkQueue。从DelayQueue的特性很容易想到它适合定时任务的实现,所以Java并发包中调度定时任务的线程池队列是基于这种实现的,它就是DelayedWorkQueue,为什么不直接使用DelayQueue而要重新实现一个DelayedWorkQueue呢,可能是了方便在实现过程中加入一些扩展。放入该延迟队列中的元素是特殊的,例如DelayedWorkQueue中放的元素是线程运行时代码RunnableScheduledFuture。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //A ScheduledFuture that is Runnable. Successful execution of the run method causes completion of the Future and allows access to its results. 2 //run方法的成功执行将导致Future的完成并允许访问其结果。 3 public interface RunnableScheduledFuture<V> extends RunnableFuture<V>, ScheduledFuture<V> { 4 5 //如果此任务是周期性的,则返回true。定期任务可以根据某些计划重新运行。非周期任务只能运行一次。 6 boolean isPeriodic(); 7 } 8 9 //A delayed result-bearing action that can be cancelled. 一种可以取消的延迟产生结果的动作。 10 //Usually a scheduled future is the result of scheduling a task with a ScheduledExecutorService. 11 //通常,ScheduledFuture是ScheduledExecutorService调度任务的结果。 12 public interface ScheduledFuture<V> extends Delayed, Future<V> { 13 //继承了Delayed接口 14 }
RunnableScheduledFuture接口继承了ScheduledFuture接口,ScheduledFuture接口继承了Delayed接口。
DelayedWorkQueue的实现没有像DelayQueue那样直接借助优先级队列来实现,而是重写了相关的逻辑但是实现的算法还是基于数组的平衡二叉堆实现,并且糅合了DelayQueue中实现延迟时间结束元素才能出队的Leader-Follower模式。可以说,DelayedWorkQueue = 优先级队列实现 + 延迟队列实现。理解DelayedWorkQueue之前请先理解PriorityBlockingQueue和DelayQueue,然后理解DelayedWorkQueue不费吹灰之力。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /** 2 * Specialized delay queue. To mesh with TPE declarations, this 3 * class must be declared as a BlockingQueue<Runnable> even though 4 * it can only hold RunnableScheduledFutures. 5 */ 6 static class DelayedWorkQueue extends AbstractQueue<Runnable> 7 implements BlockingQueue<Runnable> { 8 9 /* 10 * A DelayedWorkQueue is based on a heap-based data structure 11 * like those in DelayQueue and PriorityQueue, except that 12 * every ScheduledFutureTask also records its index into the 13 * heap array. This eliminates the need to find a task upon 14 * cancellation, greatly speeding up removal (down from O(n) 15 * to O(log n)), and reducing garbage retention that would 16 * otherwise occur by waiting for the element to rise to top 17 * before clearing. But because the queue may also hold 18 * RunnableScheduledFutures that are not ScheduledFutureTasks, 19 * we are not guaranteed to have such indices available, in 20 * which case we fall back to linear search. (We expect that 21 * most tasks will not be decorated, and that the faster cases 22 * will be much more common.) 23 * 24 * All heap operations must record index changes -- mainly 25 * within siftUp and siftDown. Upon removal, a task's 26 * heapIndex is set to -1. Note that ScheduledFutureTasks can 27 * appear at most once in the queue (this need not be true for 28 * other kinds of tasks or work queues), so are uniquely 29 * identified by heapIndex. 30 */ 31 32 private static final int INITIAL_CAPACITY = 16; 33 private RunnableScheduledFuture<?>[] queue = 34 new RunnableScheduledFuture<?>[INITIAL_CAPACITY]; 35 private final ReentrantLock lock = new ReentrantLock(); 36 private int size = 0; 37 38 /** 39 * Thread designated to wait for the task at the head of the 40 * queue. This variant of the Leader-Follower pattern 41 * (http://www.cs.wustl.edu/~schmidt/POSA/POSA2/) serves to 42 * minimize unnecessary timed waiting. When a thread becomes 43 * the leader, it waits only for the next delay to elapse, but 44 * other threads await indefinitely. The leader thread must 45 * signal some other thread before returning from take() or 46 * poll(...), unless some other thread becomes leader in the 47 * interim. Whenever the head of the queue is replaced with a 48 * task with an earlier expiration time, the leader field is 49 * invalidated by being reset to null, and some waiting 50 * thread, but not necessarily the current leader, is 51 * signalled. So waiting threads must be prepared to acquire 52 * and lose leadership while waiting. 53 */ 54 private Thread leader = null; 55 56 /** 57 * Condition signalled when a newer task becomes available at the 58 * head of the queue or a new thread may need to become leader. 59 */ 60 private final Condition available = lock.newCondition();
看到没有,类似PriorityBlockingQueue的初始化容量为16的类型为RunnableScheduledFuture的数组queue,和DelayQueue一样的非公平锁ReentrantLock和Condition,以及特有的leader线程。不同的是DelayedWorkQueue的数组元素是继承了Delayed接口的RunnableScheduledFuture接口的实现类,
入队offer
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public boolean offer(Runnable x) { 2 if (x == null) 3 throw new NullPointerException(); //不允许插入null值 4 RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x; 5 final ReentrantLock lock = this.lock; 6 lock.lock(); 7 try { 8 int i = size; 9 if (i >= queue.length) 10 grow(); //类似优先级队列PriorityBlockingQueue的扩容 11 size = i + 1; 12 if (i == 0) { //队列为空直接放在第一个位置 13 queue[0] = e; 14 setIndex(e, 0);//这是线程池定时任务特有的逻辑 15 } else { 16 siftUp(i, e); //类似优先级队列PriorityBlockingQueue的冒泡方式插入元素 17 } 18 if (queue[0] == e) { //类似延迟队列DelayQueue的消费线程唤醒与leader剥夺 19 leader = null; 20 available.signal(); 21 } 22 } finally { 23 lock.unlock(); 24 } 25 return true; 26 }
入队的逻辑综合了PriorityBlockingQueue的平衡二叉堆冒泡插入以及DelayQueue的消费线程唤醒与leader领导权剥夺。只有setIndex方法是特有的,该方法记录了元素在数组中的索引下标(在其他出队方法中,会将出队的元素的索引下标置为-1,表示已经不在队列中了),为了方便实现快速查找。它的扩容方法grow比优先级队列的实现简单粗暴多了,在持有锁的情况下每次扩容50%。siftUp与PriorityBlockingQueue的siftUpXXX方法一模一样,也只是多了一个特有的setIndex方法的调用。
出队take
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public RunnableScheduledFuture<?> take() throws InterruptedException { 2 final ReentrantLock lock = this.lock; 3 lock.lockInterruptibly(); 4 try { 5 for (;;) { 6 RunnableScheduledFuture<?> first = queue[0];//获取不移除head 7 if (first == null) //同DelayQueue一样,队列为空,等待生产者offer完唤醒 8 available.await(); 9 else { 10 long delay = first.getDelay(NANOSECONDS); 11 if (delay <= 0) //head元素已经过期 12 return finishPoll(first); 13 first = null; // don't retain ref while waiting 14 if (leader != null) //已经有leader在等待了,无限期等待leader醒来通知 15 available.await(); 16 else { // 当前线程成为leader 17 Thread thisThread = Thread.currentThread(); 18 leader = thisThread; 19 try { 20 available.awaitNanos(delay); //等待heade元素剩余的延迟时间结束 21 } finally { 22 if (leader == thisThread) 23 leader = null; //交出leader权限 24 } 25 } 26 } 27 } 28 } finally { 29 if (leader == null && queue[0] != null) //队列不为空,并且没有leader 30 available.signal(); //唤醒其它可能等待的消费线程 31 lock.unlock(); 32 } 33 } 34 35 //类似优先级队列PriorityBlockingQueue的出队逻辑 36 private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) { 37 int s = --size; 38 RunnableScheduledFuture<?> x = queue[s]; 39 queue[s] = null; 40 if (s != 0) 41 siftDown(0, x); //类似PriorityBlockingQueue的拿最后一个元素从head向下降级来确定位置 42 setIndex(f, -1); 43 return f; 44 }
出队的逻辑一样综合了PriorityBlockingQueue的平衡二叉堆向下降级以及DelayQueue的Leader-Follower线程等待唤醒模式,就不细说了。只有在finishPoll方法中,会将已经出队的RunnableScheduledFuture元素的索引下标通过setIndex设置成-1.
其它种种方法:remove、toArray、size、contains、put、drainTo(只转移延迟结束的)、peek、add、poll、clear都和DelayQueue的实现大同小异。
DelayedWorkQueue的迭代器也是同DelayQueue一样,迭代的是脱离源队列的拷贝数组,但是可以通过remove删除源队列中的对象。而且迭代器不区分元素是否延迟结束。
总结
DelayQueue延迟队列只允许放入实现了Delayed接口的实例,它是优先级队列针对计时的一种运用,所以它是基于优先级队列 + Leader-Follower的线程等待模式,只允许取出延迟结束的队列元素。获取head的线程(往往是第一个消费线程)由于head是整个队列中最先延迟结束的元素,所以线程只等待特定的剩余的延迟时间它即是leader,而其他后来的消费线程则无限期等待即follower,直到leader取走head时随机唤醒一个follower使其拿到新的head变成新的leader或者新入队了一个剩余延迟时间更短的元素导致leader失去领导权也会随机唤醒一个线程成为新的leader,如此往复等待唤醒。
至于DelayedWorkQueue也是一种设计为定时任务的延迟队列,它的实现和DelayQueue一样,不过是将优先级队列和DelayQueue的实现过程迁移到本身方法体中,从而可以在该过程当中灵活的加入定时任务特有的方法调用。