DelayQueue :支持延时获取元素的无界阻塞队列
使用 PriorityQueue 实现
队列中的元素必须实现 Delay 接口
class Message implements Delayed{
@Override
public long getDelay(TimeUnit unit) {
return 0;
}
@Override
public int compareTo(Delayed o) {
return 0;
}
}
重写其中的 getDelay(TimeUnit) 和 compareTo(Delayed) 方法
创建元素时,可以指定多久才能从队列中获取当前元素
只有在延迟期满的时才能从队列中提取元素
关注下添加元素的时候设置时间,以及获取的时候判断到期
应用场景:
① 缓存系统设计:保存缓存元素的有效期,一旦能从 DelayQueue 中获取元素,则表示缓存已失效
② 定时任务调度 :保存系统要执行的任务和任务执行时间是,一旦从 DelayQueue中获取到任务就开始执行,比如 Timer使用 DelayQueue 实现
似乎,黑马redis哪个利用消息队列的也用到了 DelayQueue, 又好像是 ArrayBlockingQueue,验证一下
1. DelayQueue 使用方式
public class DelayQueueDemo {
public static void main(String[] args) throws InterruptedException {
Message item1 = new Message("message 1", 5, TimeUnit.SECONDS);
Message item2 = new Message("message 2", 10, TimeUnit.SECONDS);
Message item3 = new Message("message 3", 15, TimeUnit.SECONDS);
DelayQueue<Message> queue = new DelayQueue<>();
queue.put(item1);
queue.put(item2);
queue.put(item3);
for(int i = 0; i < 3; i++){
Message take = queue.take();
System.out.println(take);
}
}
}
class Message implements Delayed{
private long time;
private String content;
public Message(String content, long time, TimeUnit unit){
this.content = content;
this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
}
@Override
public long getDelay(TimeUnit unit) {
return time - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
Message item = (Message) o;
long diff = this.time - item.time;
return diff <= 0 ? -1 : 1;
}
@Override
public String toString() {
return "Message { " + "time = " + time + ", content = " + content + '}';
}
}
运行结果:
Message { time = 1653636422281, content = message 1}
Message { time = 1653636427281, content = message 2}
Message { time = 1653636432281, content = message 3}
示例说明:
① Message 类实现 Delayed 类,实现其中的 getDelay(TimeUnit) 方法
由于 Delayed 接口继承了 Comparable<Delayed>
,因此还需要重写 ComparaTo(Delayed)
方法
② main 方法中创建 DelayQueue<Message>
对象,创建 message(String, long, TimeUnit)对象
③ 调用 delayQueue.put(e),delayQueue.take() 添加元素和获取元素
public interface Delayed extends Comparable<Delayed> {
// 返回值 <= 0 表示已过期
long getDelay(TimeUnit unit);
}
2. DelayQueue 属性
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 指定用于等待队列开头元素的线程,Leader-Follower模式变体,用于最小化不必要的定时等待
private Thread leader = null;
// 当更新的元素在队列头部变得可用时,或新线程可能需要成为领导者时,会发出条件信号
private final Condition available = lock.newCondition();
3. DelayQueue 构造方法
① 无参构造
public DelayQueue() {}
② 有参构造,以 Delayed 实例集合作为入参
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
调用 AbstractQueue 中的 addAll(Collection<? extends E>)
方法
public boolean addAll(Collection<? extends E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
调用DelayQueue 中重写的 add(e) 方法
public boolean add(E e) {
return offer(e);
}
此处注意下 DelayQueue 与 AbstractQueue 之间的关系
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E>
DelayQueue 继承了 AbstractQueue,但是 DelayQueue 中没有 addAll(Collection<? extends E>) 方法,有 add(e) 方法
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
什么情况下返回false 呢? 除非抛出异常,否则不会返回 false,因为未设置 return false
除非抛出异常,否则应该都会返回 true
上述过程:只有 available.signal 部分可能抛出异常,不会返回 true
4. 入队方法
4. 1 add(E e)
问题,add(), put() 都调用了 offer() 方法,注释部分为什么说会抛出 NullPointerException
异常呢?
add(E e)
public boolean add(E e) {
return offer(e);
}
4.2 put(E e)
public void put(E e) {
offer(e);
}
add(E e) 和 put(E e) 的区别为是否有返回值
add 有返回值,put 没有
add,put 都不允许添加 null 值,报异常是调用 priorityQueue.offer(E e)
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
...
return true;
}
delayQueue.put(null) 之后
Exception in thread "main" java.lang.NullPointerException
at java.util.PriorityQueue.offer(PriorityQueue.java:335)
发现是 PriorityQueue 中的异常
因 DelayQueue 数据结构是用 PriorityQueue 实现的
4.3 offer(E, long, TimeUnit)
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
问题:这里的 timeout 参数没有传递,那 offer(E, long, TimeUnit) 的意义何在?
4.4 offer(E e)
public boolean offer(E e) {
// 1. 获取可重入锁
final ReentrantLock lock = this.lock;
// 2. 加锁
lock.lock();
try {
// 3. 调用 priorityQueue.offer(e)
q.offer(e);
// 4. 优先队列的堆顶元素为添加的元素
if (q.peek() == e) {
leader = null;
available.signal();
}
// 5. 返回添加成功
return true;
} finally {
// 6. 释放锁
lock.unlock();
}
}
问题: q.ppek() == e
,为什么会执行 后面的 signal()
延时体现在哪里?
leader 的作用是什么?
4.4.1 priorityQueue.offer(e)
PriorityQueue 中的 offer() 方法
public boolean offer(E e) {
// 1. 若插入元素为null,抛出 NullPointerException 异常
if (e == null)
throw new NullPointerException();
// 2. 修改优先级队列结构性变化次数
modCount++;
// 3. 获取当前优先级队列的元素个数
int i = size;
// 4. 若当前元素个数 >= 优先级队列的容量则扩容
if (i >= queue.length)
grow(i + 1);
// 5. 队列元素个数 size++
size = i + 1;
// 6.1 若队列为空,则将元素放到数组的第0个位置
if (i == 0)
queue[0] = e;
else
// 6.2 队列不为空,则插入元素并调整小顶堆使之平衡
siftUp(i, e);
// 7. 返回插入成功
return true;
}
PriorityQueue 使用小顶堆实现
问题:PriorityQueue 中堆和数组的关系
堆就是数组实现的,只是下标间的关系用小顶堆的逻辑处理??
5. 出队方法
5.1 poll()
poll() 方法用于检索和移除队列的首个元素,若队列中没有延时过期的元素则返回 null
getDelay(TimeUnit) 方法由添加的元素对应的实体类重写 Delayed 接口中的getDelay(TimeUnit) 实现
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}
返回值:
null:队列为空,或者队列不为空时,first元素的延时时间未过期
e :first != null 且 first.getDelay(TimeUnit) < 0
5.1.1 priorityQueue.peek()
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
若队列为空则返回null,否则返回 queue数组中的第一个元素
5.1.2 delayQueue.getDelay(NANOSECONDS)
参数 TimeUnit = NANOSECONDS
NANOSECONDS {
public long toNanos(long d) { return d; }
public long toMicros(long d) { return d/(C1/C0); }
public long toMillis(long d) { return d/(C2/C0); }
public long toSeconds(long d) { return d/(C3/C0); }
public long toMinutes(long d) { return d/(C4/C0); }
public long toHours(long d) { return d/(C5/C0); }
public long toDays(long d) { return d/(C6/C0); }
public long convert(long d, TimeUnit u) { return u.toNanos(d); }
int excessNanos(long d, long m) { return (int)(d - (m*C2)); }
}
使用方法:
public Message(String content, long time, TimeUnit unit){
this.content = content;
this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
}
@Override
public long getDelay(TimeUnit unit) {
return time - System.currentTimeMillis();
}
利用 NANOSECONDS 中的 toMills(time) 将给定时间转化为 ms 为单位,对象加载的时候设定 time 为当前系统 ms时间 + 设定的转化后的ms值
在 getDelay(TimeUnit) 中比较 time 是否小于当前时间,小于则说明延时过期
测试:
① 将上述 take() 换成 poll()
Message item1 = new Message("message 1", 5, TimeUnit.SECONDS);
unit.toMills(time) = TimeUnit.SECONS.toMills(time) = 5 * 1000
因此若不加 延时,得到的结果均为 null,即当前未有元素过期
② 设置延时 60s,并调整元素添加顺序【将延迟时间短的最后添加】
Message item1 = new Message("message 1", 5, TimeUnit.SECONDS);
Message item2 = new Message("message 2", 10, TimeUnit.SECONDS);
Message item3 = new Message("message 3", 15, TimeUnit.SECONDS);
queue.put(item2);
queue.put(item3);
queue.put(item1);
输出结果:
Message { time = 1653654757632, content = message 1}
null
null
由于重写了 compareTo 方法,添加元素的时候,会根据比较器调整小顶堆,使得堆顶为最大值或最小值
若将 升序排列调整为降序排列 this.time - o.time
修改为o.time - this.time
public int compareTo(Delayed o) {
Message item = (Message) o;
long diff = this.time - item.time;
return diff <= 0 ? 1 : -1;
}
则输出结果为:
null
null
null
即PriorityQueue 调整小顶堆的时候,是根据compareTo 的返回结果,从这个角度来说,PriorityQueue 也可以实现大顶堆
③ 使用 take 检验输出顺序 ,修改 compareTo 中排序规则,改为降序
输出结果:
Message { time = 1653655662513, content = message 3}
Message { time = 1653655657513, content = message 2}
Message { time = 1653655652513, content = message 1}
getDelay :实现 Delayed 接口的实现类的实例化对象,作为添加到 DelayQueue 中的元素
5.1.3 priorityQueue.poll()
public E poll() {
// 1. 若队列为空,则返回 null
if (size == 0)
return null;
// 2. 获取删除元素后队列中剩余元素的数目,并更新 size
int s = --size;
// 3. 修该优先级队列结构性变化[添加,删除操作]次数
modCount++;
// 4. 获取队列中的首个元素即堆顶元素,并作为返回值 result
E result = (E) queue[0];
// 5. 获取队列中的最后一个元素的值
E x = (E) queue[s];
// 6. 释放数组 s 位置[第 s + 1 个元素]的空间
queue[s] = null;
// 7. 若优先级队列非空
if (s != 0)
// 7.1 向下调整优先级队列
siftDown(0, x);
// 8. 返回原优先级队列头部元素
return result;
}
5.2 poll(long, TimeUnit)
阻塞式获取,若超时或元素未过期则返回 null
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
// 1. 将设定的时间 timeout 转化为 ns 级
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
// 2. 设置可中断加锁
lock.lockInterruptibly();
try {
// 3. 循环阻塞式获取队列中的元素
for (;;) {
// 4. 获取优先级队列中的堆顶元素 first
E first = q.peek();
// 4.1 若 first == null 即队列为空
if (first == null) {
// 4.1.1 等待时间是否超时
if (nanos <= 0)
// 等待超时返回 null
return null;
else
// 4.1.2 等待未超时,则先释放锁添加到条件队列等待唤醒,唤醒后再到同步队列中排队获取锁,再执行 1 -> 4
// 线程获得锁后返回剩余等待时间
nanos = available.awaitNanos(nanos);
} else {
// 4.2 first != null 队列非空
// 4.2.1 通过重写的 comparator 方法判断延时是否过期
long delay = first.getDelay(NANOSECONDS);
// 4.2.2 延时过期,从优先队列中获取元素
if (delay <= 0)
return q.poll();
// 4.2.3 延迟未过期,判断等待时间是否超时,超时返回 null
if (nanos <= 0)
return null;
// 4.2.4 first 未过期且线程等待未超时, 设置 first = null ?
first = null; // don't retain ref while waiting
// 4.2.5 若剩余等待时间 nanos < delay 剩余到期时间, 或等待头结点的线程不为null则阻塞线程
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
// 4.2.6 剩余等待时间 nanos > delay 剩余到期时间,且等待头节点的线程为 null
// 4.2.6.1 获取当前线程
Thread thisThread = Thread.currentThread();
// 4.2.6.2 设置 leader 为当前线程
leader = thisThread;
try {
// 4.2.6.3 阻塞线程 delay ns 即刚好元素延时到期时唤醒结点,返回剩余等待时间
long timeLeft = available.awaitNanos(delay);
// 4.2.6.4 返回线程等待直到元素到期时的剩余等待时间
nanos -= delay - timeLeft;
} finally {
// 4.2.6.5 若 leader 仍未当前线程,说明当前线程醒来后获取到了元素,则设置 leader = null
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
leader :等待头部元素到期的线程
问题:什么情况下会发生 leader != thisThread 的情况?
问题:什么情况下会发生 leader != null 的情况?
注意上述 else 分支:剩余等待时间 > first 元素的剩余过期时间,线程阻塞唤醒之后,将 leader 设置为 null ,然后继续执行 for 循环中的处理逻辑,获取队列的首个元素
最后的finally 是在返回 priorityQueue.poll() 元素之前执行的
若非 leader != null 说明当前有等待队首元素的线程,则不会执行 唤醒其他线程的操作
若 leader == null 但 q.peek() == null
即队列中没有元素,也不会执行 available.signal() 操作
poll 设置阻塞式获取锁
流程图画一下:
上述还有未搞明白的地方:
关于 leader 的设置:
为什么在内层 finally 中要判断 leader == thisThread
还有就是等待头结点的线程唤醒其他结点,默认不是非公平锁吗?
还有就是 若当前线程等待消息过期时 available.awaitNanos(delay) ,锁被其他线程获取呢?当前线程是否取不到等待的消息
在生产环节会造成什么问题
5.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();
}
}
对比 poll(TimeUnit) 和 take 的流程图,基本处理逻辑相同,只是 poll(TimeUnit) 中多了等待超时的处理
以 take() 为例对获取元素的流程进行总结
① 通过q.peek()
判断队列是否为空,为空则执行 await()
take–await()无限期等待,poll(nanos)–await(nanos)等待剩余时间
② 通过first.getDelay()
获取元素的延时剩余时间,若延时过期则释放锁并执行q.poll()
从队列中获取元素
③ 元素延时未过期, poll(nanos) 会判断等待是否超时,超时则返回 null
未超时,take(), poll(nanos) 处理流程相同,本轮循环未获取到元素则设置first=null
回收
④ 通过leader != null
判断是否要阻塞线程
poll(nanos) :执行前会先判断nanos < delay
即当消息延时过期时间到时,等待时间是否超时
若超时则执行 awaitNanos(nanos)
,继续一轮for循环,直到 nanos <= 0
,等待超时返回 null
若未超时和take() 接下来的处理流程相同
take():若有正在等待头结点的线程,则阻塞线程,线程被唤醒之后继续执行 for 循环
若无正在等待头结点的线程,则通过leader - thisThread
设置当前线程为等待头节点的线程,
设置等待时间 await(delay)
等待消息过期,线程等待超时后处理 leader
【这里 if 判断还没捋清楚】
⑤ 线程等待消息过期后,再次执行for循环,获取到元素,执行 finally 解锁以及判断是否要唤醒线程后返回获取到的元素