DelayQueue是一个有界的阻塞队列,这里面的元素具有延迟释放的功能,仅仅当元素的延迟时间到了的时候才能够被获取。队列的头节点是延迟时间最久的元素。如果没有元素的延迟时间过期,这里将没有头节点,而且poll函数将返回null。这个工具主要用来标记那些应该在给定时间之后实现的对象。
注意:这个工具是线程安全的。
DelayQueue是一个阻塞的延迟队列,针对添加到其中的任务,都必须实现Delayed接口。
public interface Delayed extends Comparable {
/**
* 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);
}
从上面的Delay接口的源码我们可以看出这里有一个延迟时间,当延迟时间小于或等于0的时候,任务才能够执行。并且这里根据延迟时间的大小进行了排队,具体怎么排队的,我们在后面的源码分析部分给出,现在我们先来看一个简单的实例。
/**
* @author: LIUTAO
* @Date: Created in 2019/2/27 9:44
* @Modified By:
*/
public class DelayQueueDemo {
public static void main(String[] args) throws Exception {
BlockingQueue delayqueue = new DelayQueue<>();
long now = System.currentTimeMillis();
delayqueue.put(new Task(now + 3000));
delayqueue.put(new Task(now + 4000));
delayqueue.put(new Task(now + 8000));
delayqueue.put(new Task(now + 1000));
while (!delayqueue.isEmpty()){
System.out.println(delayqueue.take());
}
}
}
class Task implements Delayed {
long time;
public Task(long time) {
this.time = time;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(time - System.currentTimeMillis(), TimeUnit.MICROSECONDS);
}
@Override
public int compareTo(Delayed o) {
if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS))
return -1;
else if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS))
return 1;
else
return 0;
}
@Override
public String toString() {
return "time=" + time;
}
}
运行上面的程序,输出如下:
[time=1551232978716, time=1551232980716, time=1551232985716, time=1551232981716]
time=1551232978716
time=1551232980716
time=1551232981716
time=1551232985716
很明显,我们在插入数据的时候,根本没有按照延迟时间排序后进行插入,而在输出的时候,就根据延迟时间进行了排序,并且在延迟时间没有结束的时候获取操作将被阻塞。这就进一步说明了DelayQueue的功能。
下面我们开始DelayQueue的源码分析。
//保证线程安全的锁
private final transient ReentrantLock lock = new ReentrantLock();
//用于存储数据的优先队列
private final PriorityQueue q = new PriorityQueue();
//leader/follower多线程模式的leader线程
private Thread leader = null;
//条件队列的条件
private final Condition available = lock.newCondition();
针对上面的属性,我们可以看出DelayQueue底层是依赖了PriorityQueue来实现的,其实就是依靠PriorityQueue的排队特性,对DelayQueue中的任务根据延迟时间来进行排序。
这里面使用到了leader/follower多线程模式,其实就是在多个线程中只有一个leader,而其余的都是follower,仅仅leader线程可以运行。这里使用这种模式主要是为了减少等待时间或者如果全部使用固定等待时间,就为了减少锁竞争。
针对函数的分析,这里我们仅仅分析两个关键函数put和take。
put函数
put函数直接调用了offer函数,没有多余的其他逻辑。
public boolean offer(E e) {
//设置锁
final ReentrantLock lock = this.lock;
//获取锁
lock.lock();
try {
//向PriorityQueue队列中添加元素
q.offer(e);
//头节点是e
if (q.peek() == e) {
//置空leader并唤醒一个等待线程
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
上面的注释比较清除,就不进一步说明了,下面我们继续查看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;
//使用leader/follower多线程模式减少等待
if (leader != null)
available.await();
else {
//设置当前线程为leader,并等待固定时间
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();
}
}
使用PriorityQueue的peek函数来获取头节点,并判断头节点是否到期,同时使用了leader/follower多线程模式来减少等待时间。
上面就是对DelayQueue的源码分析,欢迎大家交流。谢谢!