java延迟队列DelayQueue使用及原理
概述
java延迟队列提供了在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素。没有过期元素的话,使用poll()方法会返回null值,超时判定是通过getDelay(TimeUnit.NANOSECONDS)方法的返回值小于等于0来判断。延时队列不能存放空元素。
延时队列实现了Iterator接口,但iterator()遍历顺序不保证是元素的实际存放顺序。
队列元素
DelayQueue<E extends Delayed>的队列元素需要实现Delayed接口,该接口类定义如下:
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); }
由Delayed定义可以得知,队列元素需要实现getDelay(TimeUnit unit)方法和compareTo(Delayed o)方法, getDelay定义了剩余到期时间,compareTo方法定义了元素排序规则,注意,元素的排序规则影响了元素的获取顺序,将在后面说明。
内部存储结构
DelayedQuene的元素存储交由优先级队列存放。
public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> { private final transient ReentrantLock lock = new ReentrantLock(); private final PriorityQueue<E> q = new PriorityQueue<E>();//元素存放
DelayedQuene的优先级队列使用的排序方式是队列元素的compareTo方法,优先级队列存放顺序是从小到大的,所以队列元素的compareTo方法影响了队列的出队顺序。
若compareTo方法定义不当,会造成延时高的元素在队头,延时低的元素无法出队。
获取队列元素
非阻塞获取
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(); } }
------------------------------------------------------------------------------------------------------------------------
PriorityQueue队列peek()方法。
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
由代码我们可以看出,获取元素时,总是判断PriorityQueue队列的队首元素是否到期,若未到期,返回null,所以compareTo()的方法实现不当的话,会造成队首元素未到期,当队列中有到期元素却获取不到的情况。因此,队列元素的compareTo方法实现需要注意。
阻塞方式获取
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { E first = q.peek(); if (first == null) //没有元素,让出线程,等待java.lang.Thread.State#WAITING 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();// 其它线程在leader线程TIMED_WAITING期间,会进入等待状态,这样可以只有一个线程去等待到时唤醒,避免大量唤醒操作
else { Thread thisThread = Thread.currentThread(); leader = thisThread; try { available.awaitNanos(delay);// 等待剩余时间后,再尝试获取元素,他在等待期间,由于leader是当前线程,所以其它线程会等待。 } finally { if (leader == thisThread) leader = null; } } } } } finally { if (leader == null && q.peek() != null) available.signal(); lock.unlock(); } }
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | package org.dromara.hmily.demo.springcloud.account.service; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; /** * @description: 延时队列测试 * @author: hh */ public class DelayedQueneTest { public static void main(String[] args) throws InterruptedException { Item item1 = new Item( "item1" , 5 , TimeUnit.SECONDS); Item item2 = new Item( "item2" , 10 , TimeUnit.SECONDS); Item item3 = new Item( "item3" , 15 , TimeUnit.SECONDS); DelayQueue<Item> queue = new DelayQueue<>(); queue.put(item1); queue.put(item2); queue.put(item3); System.out.println( "begin time:" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); for ( int i = 0 ; i < 3 ; i++) { Item take = queue.take(); System.out.format( "name:{%s}, time:{%s}\n" ,take.name, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)); } } } class Item implements Delayed{ /* 触发时间*/ private long time; String name; public Item(String name, long time, TimeUnit unit) { this .name = name; 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) { Item item = (Item) o; long diff = this .time - item.time; if (diff <= 0 ) { // 改成>=会造成问题 return - 1 ; } else { return 1 ; } } @Override public String toString() { return "Item{" + "time=" + time + ", name='" + name + '\ '' + '}' ; } }<br><br> |
运行结果:每5秒取出一个
1 2 3 4 | begin time: 2019 - 05 -31T11: 58 : 24.445 name:{item1}, time:{ 2019 - 05 -31T11: 58 : 29.262 } name:{item2}, time:{ 2019 - 05 -31T11: 58 : 34.262 } name:{item3}, time:{ 2019 - 05 -31T11: 58 : 39.262 } |
修改compareTo方法 diff >= 0 后的运行结果: 在15秒之后几乎同时取出,
1 2 3 4 | begin time: 2019 - 05 -31T12: 02 : 50.157 name:{item3}, time:{ 2019 - 05 -31T12: 03 : 04.959 } name:{item2}, time:{ 2019 - 05 -31T12: 03 : 04.999 } name:{item1}, time:{ 2019 - 05 -31T12: 03 : 05 } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通