作为阻塞队列的一员,DelayQueue(延迟队列)由于其特殊含义而使用在特定的场景之中,主要在于Delay这个词上,那么其内部是如何实现的呢?今天一起通过DelayQueue的源码来看一看其是如何完成Delay操作的

前言

JDK版本号:1.8.0_171

DelayQueue内部通过优先级队列PriorityQueue来实现队列元素的排序操作,之前已经介绍过PriorityBlockingQueue的源码实现,两者比较类似,可自行回顾下,既然用到了优先级队列,则需要保证其队列元素的可比较性,以及延迟队列的特性(可计算延迟时间,通过延迟时间进行比较排序),故这里其中的队列元素需要实现Delayed接口,DelayQueue主要就在于理解这两部分内容

  • DelayQueue内部通过优先级队列PriorityQueue来实现队列元素的排序操作
  • DelayQueue队列元素需要实现Delayed接口(包含compareTo接口)

使用示例

下面示例代码部分已经显示了DelayQueue的用法,从名字命名上也能理解出其含义,延迟队列,主要在于延迟消费,如何实现呢?这里就需要用到Delayed接口,后面会进行说明,在使用时需要实现Delayed接口和compareTo接口

  • 通过getDelay方法判断当前对象延迟时间是否已经到期
  • 通过compareTo方法对其队列元素排序完成其队列元素出队的先后顺序

自己可以先试试运行结果,理解看看,可以看下调用poll和take的结果。如果用过rocketmq,可以类比其中的延迟消息队列,等到规定的时间再进行消费,只不过mq中的实现要比这复杂

public class TestDelayQueue {

	public static void main(String[] args) throws InterruptedException {
DelayQueue<DelayItem> delayQueue = new DelayQueue(); // 20s后
delayQueue.add(new DelayItem(20, "aaaaaa"));
// 10秒后
delayQueue.add(new DelayItem(10, "bbbbbb"));
// 30秒后
delayQueue.add(new DelayItem(30, "cccccc")); while (0 < delayQueue.size()) {
Thread.sleep(1000);
DelayItem d = delayQueue.poll();
// DelayItem d = delayQueue.take();
System.out.println(null != d ? d.getItem() : "null");
}
} static class DelayItem implements Delayed { private long delayTime;
private String item; public DelayItem(long delayTime, String item) {
super();
// 当前时间
LocalDateTime localDateTime = LocalDateTime.now();
this.delayTime = localDateTime.getSecond() + delayTime;
this.item = item;
} @Override
public long getDelay(TimeUnit unit) {
LocalDateTime localDateTime = LocalDateTime.now();
return unit.convert(delayTime - localDateTime.getSecond(), TimeUnit.SECONDS);
} @Override
public int compareTo(Delayed o) {
return this.getDelay(TimeUnit.SECONDS) - o.getDelay(TimeUnit.SECONDS) < 0 ? -1 : 1;
} public String getItem() {
return item;
}
}
}

类定义

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

Delayed

首先要说明的是Delayed接口,类定义部分也已经明确指出其使用(E extends Delayed),我们在操作时放入DelayQueue队列元素必须实现这个接口,实现其中的getDelay方法和compareTo方法,在使用示例代码部分我也说明了这两个方法的作用

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

常量/变量

其中使用了PriorityQueue来完成有序出队操作,与之前讲解过的PriorityBlockingQueue类似,有些许不同,可自行参考源码部分,也可以去看我之前的一篇专门讲解PriorityBlockingQueue源码的文章,主要异同在于PriorityQueue是非线程安全的,而PriorityBlockingQueue是线程安全的,内部排序机制使用的都是堆排序

如果你了解过PriorityQueue或PriorityBlockingQueue则在这里使用这个类是很容易理解源码实现人员的目的的,建议先去了解其实现,要不直接看这个源码比较有难度

由于需要实现延迟队列,使用PriorityQueue根据时间排序(自行实现具体细节,例如上边示例根据时间来排序),通过Delayed接口限制使用DelayQueue的场景

    /**
* 可重入锁ReentrantLock
*/
private final transient ReentrantLock lock = new ReentrantLock();
/**
* 内部使用PriorityQueue来完成DelayQueue的操作
*/
private final PriorityQueue<E> q = new PriorityQueue<E>(); /**
* leader线程
* 指定了用于等待队列元素出队的线程
* 如果非空,则这个线程可以阻塞等待一段时间(时间通过计算获得),其他线程则无限等待
* 避免其他线程不必要的等待
* 这个线程等待一段时间然后出队操作,其他线程则无限等待,
* 如果等待过程中入队了过期时间更短的元素(优先级队列堆顶元素变化),则会重置leader为null,并会唤醒等待的线程去争抢leader来获取执行出队的权利
*/
private Thread leader = null; /**
* Condition对象完成线程等待和唤醒任务
*/
private final Condition available = lock.newCondition();

构造方法

构造方法比较简单,无参构造没有进行任何操作,有参构造方法直接传入对应类型的集合,循环add放入队列

    public DelayQueue() {}

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

重要方法

offer

入队操作,先获得lock,之后通过优先级队列的offer方法完成入队,同时判断是否要重置leader

    /**
* Inserts the specified element into this delay queue.
*
* @param e the element to add
* @return {@code true}
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
// 此节点为当前优先级队列堆顶节点,即0的索引位置
// 即此次添加的节点即为下次要获取的堆顶节点(出队节点)
// 如果非堆顶节点则表示堆顶节点未变化则不要重置leader
if (q.peek() == e) {
// leader线程置空,让出队线程争抢leader优先执行权
leader = null;
// 唤醒阻塞的线程
available.signal();
}
return true;
} finally {
lock.unlock();
}
}

poll

出队操作,先获得lock,之后通过优先级队列的poll方法完成出队,当然需要判断堆顶元素是否已到期。等待超时方法较为复杂,需耐心理解

    /**
* 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,否则通过poll出队
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}
/**
* Retrieves and removes the head of this queue, waiting if necessary
* until an element with an expired delay is available on this queue,
* or the specified wait time expires.
*
* @return the head of this queue, or {@code null} if the
* specified waiting time elapses before an element with
* an expired delay becomes available
* @throws InterruptedException {@inheritDoc}
*/
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) {
// 等待时间小于等于0则直接返回null,否则就阻塞等待nanos时间
if (nanos <= 0)
return null;
else
// 如果中途被唤醒则更新nanos,剩余等待时间
nanos = available.awaitNanos(nanos);
} else {
// 堆顶元素的延迟时间
long delay = first.getDelay(NANOSECONDS);
// 延迟时间到期直接出队操作
if (delay <= 0)
return q.poll();
// 延迟时间未到期直接返回null
if (nanos <= 0)
return null;
// 延迟时间未到期同时设置了超时时间进入下面进行处理
// 处于等待状态不要引用first
first = null; // don't retain ref while waiting
// 超时时间小于延迟时间或者leader非空阻塞等待nanos
// 超时时间小于延迟时间则当前线程最多等待nanos超时时间即可
// leader非空则表明其他线程已经获得优先执行权,最多等待nanos超时时间即可
// 在等待中有可能被唤醒再此循环执行
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
// 超时时间大于延迟时间同时leader线程为空进入下面处理
Thread thisThread = Thread.currentThread();
// 先设置leader线程获取执行权
leader = thisThread;
try {
// 阻塞等待delay即可出队操作
// 万一等待过程中被唤醒则通过剩余等待时间循环判断处理
// 有可能在等待中入队了延迟时间更短的元素,此时需释放leader重新争抢优先执行权
long timeLeft = available.awaitNanos(delay);
nanos -= delay - timeLeft;
} finally {
// 释放leader执行权,重新争抢leader
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// leader空且队列非空则唤醒其他阻塞的线程
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}

take

出队操作,先获得lock,再通过判断最终执行poll完成出队操作,和poll的超时等待方法类似

    /**
* Retrieves and removes the head of this queue, waiting if necessary
* until an element with an expired delay is available on this queue.
*
* @return the head of this queue
* @throws InterruptedException {@inheritDoc}
*/
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,等待时间内去掉引用
first = null; // don't retain ref while waiting
// leader线程非空,表示其他线程已经获取优先执行权,阻塞等待
if (leader != null)
available.await();
else {
// leader为空则指向当前线程,表示当前线程获得执行权
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 阻塞等待delay秒之后继续
// 也有可能新入队元素(堆顶元素变化时)被唤醒需重新获取leader执行权
available.awaitNanos(delay);
} finally {
// leader置空,释放优先执行权
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// leader空且队列非空则唤醒其他阻塞的线程
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}

drainTo

转移队列操作,内部是先通过peek方法先获取队列堆顶元素,判断其是否已到期,如到期则添加元素到新队列中,同时对原队列出队操作,当然,只转移已经到期的所有元素

    /**
* Returns first element only if it is expired.
* Used only by drainTo. Call only when holding lock.
*
* 命名上完全能了解其含义
* 获取队列中的堆顶元素,延迟时间还未到期则返回null
* 被drainTo所使用,参照下面方法
*
*/
private E peekExpired() {
// assert lock.isHeldByCurrentThread();
E first = q.peek();
return (first == null || first.getDelay(NANOSECONDS) > 0) ?
null : first;
} public int drainTo(Collection<? super E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
int n = 0;
// 转移已过期的元素到新队列中
for (E e; (e = peekExpired()) != null;) {
c.add(e); // In this order, in case add() throws.
q.poll();
++n;
}
return n;
} finally {
lock.unlock();
}
} public int drainTo(Collection<? super E> c, int maxElements) {
// 判空
if (c == null)
throw new NullPointerException();
// 非本对象
if (c == this)
throw new IllegalArgumentException();
// 长度判断
if (maxElements <= 0)
return 0;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int n = 0;
// 转移已过期的元素到新队列中,最多转移maxElements个元素
for (E e; n < maxElements && (e = peekExpired()) != null;) {
c.add(e); // In this order, in case add() throws.
q.poll();
++n;
}
return n;
} finally {
lock.unlock();
}
}

其他方法如peek,size,clear,toArray,remove等都是通过优先级队列PriorityQueue来实现的,只是每次操作时需要先获得可重入锁保证线程安全

迭代器

迭代器的实现不是很复杂,迭代器复制了队列中的所有元素,需要注意的是,迭代器中的remove方法会通过removeEQ方法直接删除原PriorityQueue队列中的元素,不是删除拷贝的数据元素

    /**
*
* 本质上调用PriorityQueue.toArray
* 将PriorityQueue的底层数组拷贝作为迭代器的array
* 故这里保存了所有的元素,不仅仅是已过期的元素
*/
public Iterator<E> iterator() {
return new Itr(toArray());
} /**
* Snapshot iterator that works off copy of underlying q array.
*/
private class Itr implements Iterator<E> {
// 保存PriorityQueue的数组
final Object[] array; // Array of all elements
// 下次next返回的元素索引
int cursor; // index of next element to return
// 上次返回的return元素索引
int lastRet; // index of last element, or -1 if no such Itr(Object[] array) {
lastRet = -1;
this.array = array;
} public boolean hasNext() {
return cursor < array.length;
} @SuppressWarnings("unchecked")
public E next() {
if (cursor >= array.length)
throw new NoSuchElementException();
lastRet = cursor;
return (E)array[cursor++];
} // 删除元素,需要注意,会直接把原队列中的元素删除
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
removeEQ(array[lastRet]);
lastRet = -1;
}
}

总结

DelayQueue作为一个特殊的阻塞队列,主要在于Delay特性上,内部通过优先级阻塞队列和Delayed接口实现延迟的操作,如果之前已经了解了优先级队列,则非常容易理解其源码实现逻辑,复杂点的部分也就在于在多线程环境下入队一个新的更短的元素时内部做的处理,通过争抢leader来确定优先出队的那个线程,做不同的处理,比较有意思,可以参考文章多理解理解,不算过于复杂

以上内容如有问题欢迎指出,笔者验证后将及时修正,谢谢

JDK源码那些事儿之DelayQueue的更多相关文章

  1. JDK源码那些事儿之并发ConcurrentHashMap上篇

    前面已经说明了HashMap以及红黑树的一些基本知识,对JDK8的HashMap也有了一定的了解,本篇就开始看看并发包下的ConcurrentHashMap,说实话,还是比较复杂的,笔者在这里也不会过 ...

  2. JDK源码那些事儿之红黑树基础下篇

    说到HashMap,就一定要说到红黑树,红黑树作为一种自平衡二叉查找树,是一种用途较广的数据结构,在jdk1.8中使用红黑树提升HashMap的性能,今天就来说一说红黑树,上一讲已经给出插入平衡的调整 ...

  3. JDK源码那些事儿之浅析Thread上篇

    JAVA中多线程的操作对于初学者而言是比较难理解的,其实联想到底层操作系统时我们可能会稍微明白些,对于程序而言最终都是硬件上运行二进制指令,然而,这些又太过底层,今天来看一下JAVA中的线程,浅析JD ...

  4. JDK源码那些事儿之ConcurrentLinkedDeque

    非阻塞队列ConcurrentLinkedQueue我们已经了解过了,既然是Queue,那么是否有其双端队列实现呢?答案是肯定的,今天就继续说一说非阻塞双端队列实现ConcurrentLinkedDe ...

  5. JDK源码那些事儿之ConcurrentLinkedQueue

    阻塞队列的实现前面已经讲解完毕,今天我们继续了解源码中非阻塞队列的实现,接下来就看一看ConcurrentLinkedQueue非阻塞队列是怎么完成操作的 前言 JDK版本号:1.8.0_171 Co ...

  6. JDK源码那些事儿之LinkedBlockingDeque

    阻塞队列中目前还剩下一个比较特殊的队列实现,相比较前面讲解过的队列,本文中要讲的LinkedBlockingDeque比较容易理解了,但是与之前讲解过的阻塞队列又有些不同,从命名上你应该能看出一些端倪 ...

  7. JDK源码那些事儿之LinkedTransferQueue

    在JDK8的阻塞队列实现中还有两个未进行说明,今天继续对其中的一个阻塞队列LinkedTransferQueue进行源码分析,如果之前的队列分析已经让你对阻塞队列有了一定的了解,相信本文要讲解的Lin ...

  8. JDK源码那些事儿之SynchronousQueue上篇

    今天继续来讲解阻塞队列,一个比较特殊的阻塞队列SynchronousQueue,通过Executors框架提供的线程池cachedThreadPool中我们可以看到其被使用作为可缓存线程池的队列实现, ...

  9. JDK源码那些事儿之PriorityBlockingQueue

    今天继续说一说阻塞队列的实现,今天的主角就是优先级阻塞队列PriorityBlockingQueue,从命名上看觉得应该是有序的,毕竟是优先级队列,那么实际上是什么情况,我们一起看下其内部实现,提前说 ...

随机推荐

  1. elasticsearch的数据写入流程及优化

    Elasticsearch 写入流程及优化 一. 集群分片设置:ES一旦创建好索引后,就无法调整分片的设置,而在ES中,一个分片实际上对应一个lucene 索引,而lucene索引的读写会占用很多的系 ...

  2. spring security的BCryptPasswordEncoder加密和对密码验证的原理

    目录 BCryptPasswordEncoder加密和对密码验证的原理 一.加密算法和hash算法的区别 二.源码解析 1. encode方法 2. BCrypt.hashpw方法 3. matche ...

  3. Python Tkinter 之Listbox控件

    Listbox为列表框控件,它可以包含一个或多个文本项(text item),可以设置为单选或多选.使用方式为Listbox(root,option...). 常用的参数列表如下: 一些常用的函数:

  4. A+B for Polynomials

    This time, you are supposed to find A+B where A and B are two polynomials. Input Specification: Each ...

  5. LeetCode 32. 最长有效括号(Longest Valid Parentheses) 31

    32. 最长有效括号 32. Longest Valid Parentheses 题目描述 给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度. 每日一算法2019/6/ ...

  6. 13 Spring 的事务控制

    1.事务的概念 理解事务之前,先讲一个你日常生活中最常干的事:取钱.  比如你去ATM机取1000块钱,大体有两个步骤:首先输入密码金额,银行卡扣掉1000元钱:然后ATM出1000元钱.这两个步骤必 ...

  7. SpringMVC笔记1

    SpringMVC是一个一种基于Java的实现MVC设计模型的请求驱动类型的轻量级web框架 SpringMVC的入门案例 2.导入相关jar包 <?xml version="1.0& ...

  8. jstack的使用:死锁问题实战

  9. Tkinter & mysql 的登录框练习

    import tkinter as tk from tkinter import messagebox import pymysql class SignIn(object): def __init_ ...

  10. CacheManager.Core

    GitHub地址:https://github.com/MichaCo/CacheManager CacheManager的优点: 让开发人员的生活更容易处理和配资缓存,即使是非常复杂的缓存方案. C ...