DelayQueue源码解析
与大多数阻塞队列不同,DelayQueue是一种延迟队列。一般的队列当资源足够时立马执行,而DelayQueue到达设定的延迟时间后才会执行。
1.整体结构
DelayQueue延迟队列底层使用的是锁的能力,在多线程操作时也具备安全性。
DelayQueue类注释中提及的3个关键概念,
- DelayQueue底层是一个无界队列,队列中元素将在过期时被获取到。越靠近队头的元素,过期时间越早。
- 没有过期的元素不能被从队列中取出
- 不允许空元素放入队列中
DelayQueue的队列中保存的元素是有规定的,具体可以从其类定义中看出,
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
队列中元素必须是Delayed的子类,Delayed是延迟能力的关键接口,该接口继承了Comparable接口,同时定义了getDelay方法用于获取距离过期所剩余的时间,
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
所以DelayQueue队列中存放的元素必须实现Delayed和Comparable接口,并复写getDelay和compareTo方法,否则编译过程中会提示出错。
此外,DelayQueue大量使用了PriorityQueue队列的大量功能,具体示例如下,
PriorityQueue是优先级队列,此处的作用就是根据过期时间做优先级排序,使得先过期的可以先执行,这种设计思路是十分巧妙的。
2.使用演示
使用下方的demo对DelayQueue的使用进行演示,
public class DelayQueueDemo {
// 队列消息的生产者
static class Product implements Runnable {
private final BlockingQueue queue;
public Product(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try {
log.info("begin put");
long beginTime = System.currentTimeMillis();
queue.put(new DelayedDTO(System.currentTimeMillis() + 2000L,beginTime));//延迟 2 秒执行
queue.put(new DelayedDTO(System.currentTimeMillis() + 5000L,beginTime));//延迟 5 秒执行
queue.put(new DelayedDTO(System.currentTimeMillis() + 1000L * 10,beginTime));//延迟 10 秒执行
log.info("end put");
} catch (InterruptedException e) {
log.error("" + e);
}
}
}
// 队列的消费者
static class Consumer implements Runnable {
private final BlockingQueue queue;
public Consumer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try {
log.info("Consumer begin");
((DelayedDTO) queue.take()).run();
((DelayedDTO) queue.take()).run();
((DelayedDTO) queue.take()).run();
log.info("Consumer end");
} catch (InterruptedException e) {
log.error("" + e);
}
}
}
@Data
// 队列元素,实现了 Delayed 接口
static class DelayedDTO implements Delayed {
Long s;
Long beginTime;
public DelayedDTO(Long s,Long beginTime) {
this.s = s;
this.beginTime =beginTime;
}
@Override // 复写getDelay方法
public long getDelay(TimeUnit unit) {
return unit.convert(s - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override // 复写compareTo方法
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
public void run(){
log.info("现在已经过了{}秒钟",(System.currentTimeMillis() - beginTime)/1000);
}
}
// demo 运行入口
public static void main(String[] args) throws InterruptedException {
BlockingQueue q = new DelayQueue();
DelayQueueDemo.Product p = new DelayQueueDemo.Product(q);
DelayQueueDemo.Consumer c = new DelayQueueDemo.Consumer(q);
new Thread(c).start();
new Thread(p).start();
}
}
主函数执行结果,
06:57:50.544 [Thread-0] Consumer begin
06:57:50.544 [Thread-1] begin put
06:57:50.551 [Thread-1] end put
06:57:52.554 [Thread-0] 延迟了2秒钟才执行
06:57:55.555 [Thread-0] 延迟了5秒钟才执行
06:58:00.555 [Thread-0] 延迟了10秒钟才执行
06:58:00.556 [Thread-0] Consumer end
实际开发过程中使用DelayQueue的思路大致如此,多线程环境中向队列添加不同的元素,等待延迟时间过后执行相应的方法。
3. 源码解析
数据存入队列—put方法
put方法底层调用的是offer方法,源码如下,
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
// 上锁
lock.lock();
try {
// 使用 PriorityQueue 的扩容和排序等能力
q.offer(e);
// 如果恰好刚放进去的元素正好在队列头,
// 立马唤醒因之前队列为空时 take 的阻塞线程,执行 take 操作
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
// 释放锁
lock.unlock();
}
}
插入新元素时调用了 PriorityQueue的offer方法,
public boolean offer(E e) {
// 如果是空元素的话,抛异常
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
// 队列实际大小大于容量时,进行扩容
// 扩容策略是:如果原容量小于64,则2倍扩容;反之,50 % 扩容
if (i >= queue.length)
grow(i + 1);
size = i + 1;
// 如果队列为空,当前元素正好处于队头
if (i == 0)
queue[0] = e;
else
// 如果队列不为空,需要根据优先级进行排序
siftUp(i, e);
return true;
}
// 按照从小到大的顺序排列
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
// k 是当前队列实际大小的位置
while (k > 0) {
// 对 k 进行减倍
int parent = (k - 1) >>> 1;
Object e = queue[parent];
// 如果 x 比 e 大,退出,把 x 放在 k 位置上
if (key.compareTo((E) e) >= 0)
break;
// x 比 e 小,继续循环,直到找到 x 比队列中元素大的位置
queue[k] = e;
k = parent;
}
queue[k] = key;
}
排序过程中调用了DelayQueue队列元素定义过程中复写的compareTo方法。
队列取出数据—take方法
取元素时,如果发现有元素到达过期时间,就直接取出;反之,则线程一直阻塞。
take方法的核心源码如下,
for (;;) {
// 从队头中拿数据出来
E first = q.peek();
// 如果为空,说明队列中没有数据,take方法阻塞
if (first == null)
available.await();
else {
// 获取队头元素距离设置的过期时间所剩的时间
long delay = first.getDelay(NANOSECONDS);
// 如果过期了,直接返回队头数据
if (delay <= 0)
return q.poll();
// 引用置为null ,便于gc,这样可以让线程等待时,回收 first 变量
first = null;
// leader 不为空表示当前队列元素之前已经被设置过阻塞时间了
// 直接阻塞当前线程等待。
if (leader != null)
available.await();
else {
// 之前没有设置过阻塞时间,按照一定的时间进行阻塞
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 进行阻塞
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
take方法当队列中队头始终没有元素或队头元素很长时间没有到达过期时间的情况下会无限阻塞,如果不想无限阻塞,可以使用poll方法,设置超时时间,在超时时间内,队头元素还没有过期则返回null。