1. 概述
DelayQueue 是 BlockingQueue 接口的一个实现,它属于无界阻塞队列 。其最核心的特征是:队列中的元素只有在其指定的延迟时间到期之后,才能被消费者从队列中取出。
DelayQueue 的内部实现并非从零开始,而是巧妙地利用了一个已有的数据结构------PriorityQueue(优先级队列,底层为二叉堆)作为存储容器。队列中的元素必须实现 Delayed 接口,该接口定义了获取剩余延迟时间的方法 getDelay(TimeUnit unit)。内部 PriorityQueue 会根据元素的剩余延迟时间进行排序,队首元素永远是剩余延迟时间最短(即最早过期)的那个。
核心特点:
- 无界性 :从逻辑上讲,
DelayQueue是无界的。生产者插入元素永远不会因为队列满而阻塞(put不阻塞)。但这并不意味着它可以无限存储,其容量上限受限于 JVM 堆内存大小。 - 元素必须实现
Delayed:这是强制要求。Delayed接口继承自Comparable,因此元素必须具备可比较性和获取剩余延迟的能力。 - 延时获取 :消费者线程在调用
take()方法时,如果队列为空,或者队首元素尚未过期,线程将进入阻塞状态,直到有元素过期可用。 - 单锁设计 :内部使用一把
ReentrantLock来保证线程安全,所有对底层PriorityQueue的操作都在锁的保护下进行。 - 禁止
null元素 :和大多数阻塞队列一样,null元素不被接受。
与其他阻塞队列的初步对比:
- 与
PriorityBlockingQueue:两者关系密切。DelayQueue可以看作是PriorityBlockingQueue的一个特化版本。二者都基于PriorityQueue实现,都使用单锁。区别在于DelayQueue在消费者获取元素时增加了时间维度的检查(检查元素是否过期),从而引入了更复杂的阻塞/唤醒机制(Leader-Follower 模式)。 - 与
ArrayBlockingQueue/LinkedBlockingQueue:后两者是典型的 FIFO(先进先出)队列,不关心元素的优先级,也不关心时间。ArrayBlockingQueue是有界的,生产者可能会因队列满而阻塞;LinkedBlockingQueue可选有界(默认无界)。而DelayQueue的核心竞争点在于时间优先级 和延时阻塞。
典型应用场景:
- 缓存超时管理 :利用
DelayQueue存储缓存项的过期时间,后台线程循环take()清理过期缓存。 - 定时任务调度:将任务包装为延时元素放入队列,消费者线程在任务到期时执行它。
- 限时重试队列:操作失败后,将重试任务放入队列,设置一定的重试间隔。
- 会话超时清理:Web 容器中管理用户会话的超时失效。
2. 核心方法说明
下表汇总了 DelayQueue 主要方法的签名、参数、行为及异常情况。
| 方法 | 参数 | 返回值 | 阻塞行为 | 异常 |
|---|---|---|---|---|
DelayQueue() |
无 | 构造器,内部创建 PriorityQueue |
无 | 无 |
DelayQueue(Collection<? extends E> c) |
初始集合 | 构造器,将集合元素添加到队列(元素必须实现 Delayed) |
无 | NullPointerException 如果集合或其元素为 null;ClassCastException 如果元素未实现 Delayed |
put(E e) |
e:元素 |
void |
不阻塞 (无界队列),直接插入,内部调用 offer |
NullPointerException、ClassCastException |
offer(E e) |
e:元素 |
boolean:总是返回 true(无界) |
不阻塞 | NullPointerException、ClassCastException |
offer(E e, long timeout, TimeUnit unit) |
元素、超时时间、单位 | boolean:总是返回 true(超时参数被忽略) |
不阻塞 | NullPointerException、ClassCastException |
take() |
无 | E:队首已过期的元素 |
如果队列空或队首未过期,线程阻塞直到队首过期或被唤醒 | InterruptedException |
poll() |
无 | E:队首已过期元素,若队列空或队首未过期返回 null |
不阻塞 | 无 |
poll(long timeout, TimeUnit unit) |
超时时间、单位 | E:元素,超时后仍无可取元素返回 null |
等待指定时间,期间可能被中断 | InterruptedException |
peek() |
无 | E:队首元素(不移除),不检查是否过期 ,空返回 null |
不阻塞 | 无 |
size() |
无 | int:当前元素总个数(包括未过期的) |
无 | 无 |
remainingCapacity() |
无 | int:总是返回 Integer.MAX_VALUE(因为无界) |
无 | 无 |
drainTo(Collection<? super E> c) |
目标集合 | int:转移的元素数量(仅转移已过期的) |
无 | NullPointerException |
drainTo(Collection<? super E> c, int maxElements) |
目标集合、最大转移数 | int:实际转移数(仅转移已过期的) |
无 | NullPointerException |
3. 核心原理与源码分析(基于 JDK 8)
3.1 数据结构
DelayQueue 的内部结构非常精简,主要依赖于以下四个核心字段:
java
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
// 1. 全局可重入锁,保证线程安全
private final transient ReentrantLock lock = new ReentrantLock();
// 2. 底层存储容器,基于二叉堆实现的优先级队列
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 3. Leader-Follower 模式的核心:指向当前正在等待队首元素过期的线程
private Thread leader = null;
// 4. 条件变量,用于阻塞和唤醒消费者线程
private final Condition available = lock.newCondition();
}
lock:ReentrantLock实例,所有对队列的访问和修改操作都必须先获取此锁。与PriorityBlockingQueue一样,采用单锁设计,实现简单,但在极高并发下可能成为瓶颈。q:PriorityQueue<E>实例。DelayQueue并不自己实现堆算法,而是直接复用PriorityQueue。由于元素实现了Delayed接口(继承自Comparable),PriorityQueue会根据元素实现的compareTo方法自动维护堆序,使得延时最短的元素位于堆顶。leader:Thread类型变量。这是DelayQueue实现高效等待的核心。它表示当前第一个 因为队首元素未过期而进入定时等待的线程。后续的消费者线程看到leader不为null时,将直接进入无限期等待,从而避免了大量线程同时进行定时等待和无效唤醒(惊群效应)。available:绑定在lock上的条件变量。当队列为空或队首元素未过期时,消费者线程在available上等待;当生产者插入一个更早过期的元素(成为新队首)或者消费者取走一个元素后,会通过available.signal()唤醒一个等待线程。
3.2 Delayed 接口
存入 DelayQueue 的元素必须实现 java.util.concurrent.Delayed 接口。
java
public interface Delayed extends Comparable<Delayed> {
/**
* 返回与此对象相关的剩余延迟时间,以给定的时间单位表示。
* @param unit 时间单位
* @return 剩余延迟时间;零或负值表示延迟已经过去
*/
long getDelay(TimeUnit unit);
}
继承 Comparable 意味着元素必须实现 compareTo 方法。通常的实现规范是:compareTo 方法的排序逻辑应与 getDelay 方法保持一致。也就是说,剩余延时越短的元素,在排序时应越"小",从而排在堆顶。例如:
java
public int compareTo(Delayed other) {
return Long.compare(this.getDelay(TimeUnit.NANOSECONDS),
other.getDelay(TimeUnit.NANOSECONDS));
}
3.3 构造器
java
// 无参构造器
public DelayQueue() {}
// 基于集合的构造器
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
addAll 方法内部会遍历集合并调用 offer 方法添加元素。如果集合中的任何元素为 null 或未实现 Delayed,则会抛出相应异常。
3.4 插入元素(offer / put)
由于 DelayQueue 是无界的,插入操作从不阻塞,put 和带超时的 offer 均直接委托给核心的 offer(E e) 方法。
java
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 1. 调用底层 PriorityQueue 的 offer 方法插入元素(会进行堆上浮调整)
q.offer(e);
// 2. 检查新插入的元素是否成为了堆顶(即剩余延时最小)
if (q.peek() == e) {
// 3. 如果是,说明有一个更早过期的元素加入。需要重置 leader 并唤醒一个等待的消费者线程
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
源码分析:
- 第 2 行的判断
q.peek() == e非常巧妙。由于PriorityQueue是一个最小堆,peek()返回堆顶元素。如果刚插入的元素就是堆顶,说明它的过期时间比原来堆顶的过期时间还要早。 - 此时必须执行
leader = null; available.signal();:leader = null;:原来的leader线程正在等待一个相对较晚的时间,但现在有了更早过期的元素,原leader的等待目标已失效,所以将其置空。available.signal();:唤醒一个正在等待的消费者线程。被唤醒的线程会重新检查新的堆顶元素,并根据其过期时间决定是立即取出还是重新成为leader进行定时等待。
3.5 取出元素(take)
take() 方法是 DelayQueue 的精髓,也是实现最复杂的部分,它包含了完整的 Leader-Follower 模式实现。
java
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 响应中断
try {
for (;;) { // 自旋,防止虚假唤醒
// 1. 获取堆顶元素
E first = q.peek();
if (first == null) {
// 队列为空,无限期等待,直到被生产者 signal
available.await();
} else {
// 2. 获取堆顶元素的剩余延迟时间(单位:纳秒)
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0) {
// 3. 延迟时间已到(或已过期),直接取出并返回
return q.poll();
}
// 4. 延迟时间未到,需要等待
first = null; // 显式置空,防止内存泄漏
// 5. Leader-Follower 模式决策分支
if (leader != null) {
// 如果已经有 leader 线程在等待这个堆顶元素,当前线程作为 follower 无限期等待
available.await();
} else {
// 当前没有 leader,此线程成为 leader
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// leader 线程进行限时等待,等待剩余延迟时间
available.awaitNanos(delay);
} finally {
// 等待结束(到期或被中断),重置 leader
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 6. 退出循环后的清理工作
if (leader == null && q.peek() != null) {
// 如果当前没有 leader 且队列中还有元素,唤醒一个等待的消费者线程来处理
available.signal();
}
lock.unlock();
}
}
Leader-Follower 模式详解:
DelayQueue 采用此模式是为了避免惊群效应。
- 如果没有
leader:假设有 5 个消费者线程同时调用take(),而此时堆顶元素的延时为 1 秒。这 5 个线程都会计算出delay = 1s,然后各自调用available.awaitNanos(delay)。1 秒后,5 个线程被同时唤醒,它们重新竞争锁,但只有一个线程能成功取出元素,其余 4 个线程发现堆顶元素已被取走或又有新的未过期堆顶,于是再次进入等待。这种大量的无效唤醒和上下文切换会严重消耗 CPU 资源。 - 有了
leader后 :只有第一个进入等待的线程会作为leader,并调用available.awaitNanos(delay)进行精准定时等待。后续的 4 个线程看到leader != null,直接调用available.await()进入无限期等待。1 秒后,leader线程被唤醒,取出元素,并在finally块中发现leader == null且队列非空,于是执行available.signal()唤醒其中一个无限期等待的 follower 线程。该 follower 线程成为新的leader并检查新的堆顶元素。这样,每次过期事件最多只唤醒两个线程(leader 和下一个 follower),极大减少了系统开销。
3.6 非阻塞与超时取出(poll)
java
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
// 如果队列为空,或堆顶元素未过期,直接返回 null
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}
超时 poll(long timeout, TimeUnit unit) 的实现与 take() 非常相似,区别在于它使用 awaitNanos 实现总体的超时控制,并且在等待过程中会扣减已等待的时间,超时后返回 null。其中也应用了 Leader-Follower 模式来优化。
3.7 移除元素与批量转移(drainTo)
remove(Object o):直接调用q.remove(o),移除成功且移除的是堆顶元素时,可能会执行available.signal()唤醒等待线程。drainTo:该方法用于一次性将队列中所有已过期的元素转移到另一个集合中。
java
public int drainTo(Collection<? super E> c, int maxElements) {
if (c == null) throw new NullPointerException();
if (c == this) throw new IllegalArgumentException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
int n = 0;
// 循环条件:未达到最大转移数,且堆顶元素存在,且堆顶元素已过期
while (n < maxElements && q.peek() != null && q.peek().getDelay(NANOSECONDS) <= 0) {
c.add(q.poll()); // 弹出并添加到集合
++n;
}
return n;
} finally {
lock.unlock();
}
}
注意 :drainTo 在持有锁的情况下一次性转移多个元素,这在处理批量过期任务时比多次调用 poll 效率更高。但它不会转移任何未过期的元素,即使目标集合还有剩余空间。
4. 必要流程
4.1 类图
详细文字说明:
上图展示了 DelayQueue 在 JDK 并发框架中的静态结构关系。
- 继承体系 :
DelayQueue继承自AbstractQueue抽象类,并实现了BlockingQueue接口。这使得它既具备普通队列的基础骨架,又拥有阻塞队列的特定语义(如可中断的take方法)。 - 元素约束 :队列元素
E必须实现Delayed接口。Delayed继承自Comparable,因此元素天然具备可比较性 和获取剩余延时 的能力。这是DelayQueue能够进行优先级排序和延时判断的基石。 - 核心组合关系 :
DelayQueue包含 一个PriorityQueue实例(q),这是实际存储数据的容器。DelayQueue将复杂的堆排序逻辑完全委托给PriorityQueue,自己只专注于并发控制和延时等待逻辑。DelayQueue依赖ReentrantLock(lock)和Condition(available)来实现线程同步。leader字段虽然类型为Thread,但在类图中通常不作为独立组合关系画出,它是优化消费者等待行为的关键状态变量。
此结构清晰体现了 "组合优于继承" 的设计原则:通过组合一个现有的非线程安全容器(PriorityQueue)并外挂同步机制,安全地扩展了其功能。
4.2 元素存储结构图
详细文字说明:
该图揭示了 DelayQueue 内部数据的物理存储形态。
- 底层数据结构 :
PriorityQueue内部维护一个Object[]数组。这个数组并非杂乱无章,而是被组织成了一个二叉最小堆(Binary Min-Heap)。 - 堆序性质 :在最小堆中,任何一个父节点的"值"都小于或等于其子节点的"值"。对于
DelayQueue而言,这个"值"就是元素通过compareTo方法比较出来的剩余延迟时间 。因此,数组索引[0]的位置(堆顶)永远存储着剩余延迟时间最小的元素。 - 实例说明 :图中展示了三个元素 A、B、C。虽然物理顺序是 C、A、B,但逻辑顺序由堆决定。C 的剩余延迟为 50ms,是三者中最小的,因此它占据堆顶
[0]的位置。A (100ms) 和 B (200ms) 作为子节点(或更深层节点)排列在后方。 - 关键结论 :消费者线程无论是通过
take还是poll获取元素,JDK 代码总是调用q.peek()来检查索引[0]处的元素。这种结构保证了以 O(1) 的时间复杂度即可获取到最早过期的元素。
4.3 插入元素(offer)流程图
详细文字说明:
该流程图详细描述了生产者线程调用 offer 方法插入元素时的内部执行路径。
- 加锁 :所有对底层
PriorityQueue的操作都必须在锁保护下进行,以保证堆结构的线程安全性。 - 堆插入 :调用
q.offer(e)将元素放入数组末尾,并执行上浮(Sift Up) 操作。JDK 会根据元素的compareTo结果,将其与父节点比较、交换,直到找到合适位置,维持最小堆性质。此步骤时间复杂度为 O(log n)。 - 关键决策点:是否成为新堆顶?
- 代码通过
q.peek() == e来判断。这是最巧妙的一步。 - 如果刚插入的元素
e就是当前的堆顶元素,意味着它的过期时间比原队列中所有元素都要早。此时,等待条件发生了根本性变化。
- 代码通过
- 重置 Leader 并唤醒 :
leader = null;:原有正在等待某个较晚过期时间的leader线程(如果有的话)需要被"废黜",因为它的等待目标已不是最优解。available.signal();:唤醒一个正在available条件上等待的消费者线程。被唤醒的线程会重新竞争锁,并检查新的堆顶元素,从而及时响应这个更早到期的元素。
- 解锁返回 :无论是否成为堆顶,插入操作本身永远不会失败,最终解锁并返回
true。
4.4 取出元素(take)的完整时序图
详细文字说明:
该时序图模拟了多线程环境下 take() 方法可能遇到的四种核心场景,是理解 Leader-Follower 模式的关键。
-
场景一(队列空) :消费者 T1 获取锁后发现
peek()为null。由于没有元素可用,它调用available.await()释放锁并进入无限期休眠 ,直到有生产者插入元素后通过signal唤醒它。 -
场景二(队首已过期) :T1 获取锁后发现堆顶元素的
delay <= 0。这是一个快速路径 。T1 直接调用q.poll()将元素弹出并返回。在解锁前,如果队列中还有其他元素(无论是否过期),T1 会执行一次available.signal(),用以唤醒一个可能正在无限等待的 Follower 线程,让其接替处理后续任务。 -
场景三(队首未过期,无 Leader) :这是成为 Leader 的过程。T1 发现
delay > 0,且leader == null。它将自己设置为leader,并调用available.awaitNanos(delay)。注意 :此时锁被释放,T1 在精确的剩余延迟时间内休眠,不会阻塞其他生产者的插入操作。当延时到期,T1 会被操作系统唤醒并重新竞争锁,成功后继续执行取出逻辑,最后在finally块中唤醒下一个 Follower。 -
场景四(队首未过期,已有 Leader) :这是 Follower 的宿命 。T2 获取锁后发现堆顶元素尚未过期,但
leader字段已经有值(指向 T1)。这意味着已经有一个线程(T1)负责等待这个元素的过期事件了。T2 作为 Follower,为了避免无效竞争,直接调用available.await()进入无限期等待,等待 T1 成功取出元素后来唤醒它。
4.5 超时 poll 的等待-超时恢复流程
详细文字说明:
该流程图展示了带超时参数的 poll 方法的决策逻辑。它融合了 take 的 Leader-Follower 机制与超时控制。
- 总超时管理 :流程图中引入了
剩余等待时间这一变量。初始值为用户传入的timeout。 - 队列为空时的等待 :如果队列为空且剩余时间大于 0,线程会进入限时等待。一旦被唤醒,会重新计算剩余时间。若剩余时间归零,则直接返回
null。 - 元素未过期的策略选择 :
- 情况 A :堆顶元素剩余的过期时间
delay大于 用户允许的剩余等待时间。此时等待变得没有意义(等不到了),线程直接进入上述的空队列超时等待逻辑,最终可能因超时返回null。 - 情况 B :
delay小于剩余等待时间。此时线程有机会等到元素过期。它进入 Leader-Follower 分支:- 若有 Leader,当前线程作为 Follower 无限等待。
- 若无 Leader,当前线程成为 Leader,等待
delay纳秒。
- 情况 A :堆顶元素剩余的过期时间
- 时间扣减 :成为 Leader 的线程在从
awaitNanos(delay)返回后,必须将 实际经过的时间 从总剩余时间中扣除。这确保了即使发生虚假唤醒,总超时控制依然精准。
4.6 Leader-Follower 优化示意图
详细文字说明:
这张对比图直观地展示了 Leader-Follower 模式如何解决多线程并发等待延迟任务时的性能瓶颈。
- 左侧:无优化(惊群效应) 。假设有 3 个线程都在等待同一个 1 秒后过期的任务。操作系统定时器到期时,这 3 个线程会同时 从内核态被唤醒。它们瞬间涌向
DelayQueue的锁,发生激烈竞争。最终只有一个线程获胜拿到元素,另外两个线程抢锁失败再次进入阻塞。这种大规模的无效唤醒和上下文切换会导致 CPU 飙升,吞吐量下降。 - 右侧:Leader-Follower 优化 。在
DelayQueue中,只有 Leader 线程在等待 1 秒的定时器。其余 Follower 线程处于无限期休眠 状态(不占用 CPU 时间片,不参与定时器调度)。1 秒后,仅 Leader 被唤醒,它完成任务后,显式地唤醒 一个 Follower。被唤醒的 Follower 晋升为新的 Leader 去检查新的队首元素。整个过程像接力赛一样链式传递 ,将每次过期事件触发的线程唤醒数量从N降低到了常数2。
4.7 drainTo 批量消费的内部循环流程图
详细文字说明:
该流程图描述了 drainTo 方法一次性转移多个过期元素的高效流程。
- 持锁操作 :与
take或poll每次操作都加锁解锁不同,drainTo在整个转移过程中仅持有一次锁。这避免了频繁的锁竞争,极大提升了批量处理的吞吐量。 - 紧凑循环 :流程在一个
while循环中进行,循环条件严格限制了三个边界:- 未达到最大转移数量
maxElements。 - 队列非空(
peek不为空)。 - 且 堆顶元素已过期(
getDelay <= 0)。
- 未达到最大转移数量
- 遇到未过期即停止 :这是
drainTo与普通阻塞队列最显著的区别。一旦peek()检查发现堆顶元素还未过期,循环立即终止。它不会等待元素过期,也不会跳过未过期元素去检查堆中更深层的元素(因为堆的性质保证了堆顶最小,堆顶未过期则所有元素都未过期)。 - 批量添加 :每当一个过期元素被
q.poll()弹出后,立即通过c.add()转移到目标集合中。最后返回实际转移的数量n。
5. 实际应用场景与代码举例(JDK 8 兼容)
5.1 缓存超时管理
java
import java.util.concurrent.*;
import java.util.Map;
import java.util.HashMap;
// 1. 定义缓存条目,实现 Delayed 接口
class CacheEntry<K, V> implements Delayed {
private final K key;
private final V value;
private final long expireTime; // 过期时间点(纳秒)
public CacheEntry(K key, V value, long ttl, TimeUnit unit) {
this.key = key;
this.value = value;
this.expireTime = System.nanoTime() + unit.toNanos(ttl);
}
public K getKey() { return key; }
public V getValue() { return value; }
@Override
public long getDelay(TimeUnit unit) {
long delay = expireTime - System.nanoTime();
return unit.convert(delay, TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
if (o instanceof CacheEntry) {
CacheEntry<?, ?> other = (CacheEntry<?, ?>) o;
return Long.compare(this.expireTime, other.expireTime);
}
return 0;
}
}
// 2. 缓存管理器
class TimedCache<K, V> {
private final Map<K, V> cache = new ConcurrentHashMap<>();
private final DelayQueue<CacheEntry<K, V>> delayQueue = new DelayQueue<>();
private volatile boolean running = true;
public TimedCache() {
// 启动清理线程
Thread cleaner = new Thread(() -> {
while (running) {
try {
CacheEntry<K, V> entry = delayQueue.take();
cache.remove(entry.getKey(), entry.getValue());
System.out.println("清理过期缓存: " + entry.getKey());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
cleaner.setDaemon(true);
cleaner.start();
}
public void put(K key, V value, long ttl, TimeUnit unit) {
V old = cache.put(key, value);
if (old != null) {
// 简单起见,这里不处理旧条目的移除,实际需用唯一标识
}
delayQueue.put(new CacheEntry<>(key, value, ttl, unit));
System.out.println("添加缓存: " + key + " TTL: " + ttl + " " + unit);
}
public V get(K key) {
return cache.get(key);
}
public void stop() {
running = false;
}
}
// 测试主类
public class CacheExample {
public static void main(String[] args) throws InterruptedException {
TimedCache<String, String> cache = new TimedCache<>();
cache.put("user:1", "Alice", 2, TimeUnit.SECONDS);
cache.put("user:2", "Bob", 4, TimeUnit.SECONDS);
Thread.sleep(2500);
System.out.println("2.5s 后查询 user:1 -> " + cache.get("user:1")); // 已过期,null
System.out.println("2.5s 后查询 user:2 -> " + cache.get("user:2")); // 未过期,Bob
Thread.sleep(2000);
System.out.println("4.5s 后查询 user:2 -> " + cache.get("user:2")); // 已过期,null
cache.stop();
}
}
5.2 定时任务调度
java
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
// 1. 定义定时任务
class ScheduledTask implements Delayed {
private final Runnable command;
private final long triggerTime; // 触发时间点(纳秒)
public ScheduledTask(Runnable command, long delay, TimeUnit unit) {
this.command = command;
this.triggerTime = System.nanoTime() + unit.toNanos(delay);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(triggerTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
if (o instanceof ScheduledTask) {
return Long.compare(this.triggerTime, ((ScheduledTask) o).triggerTime);
}
return 0;
}
public void run() {
command.run();
}
}
// 2. 简单调度器
class SimpleScheduler {
private final DelayQueue<ScheduledTask> queue = new DelayQueue<>();
private volatile boolean running = true;
private final ExecutorService executor = Executors.newCachedThreadPool();
public SimpleScheduler() {
Thread worker = new Thread(() -> {
while (running) {
try {
ScheduledTask task = queue.take();
executor.submit(task::run);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
executor.shutdown();
});
worker.setDaemon(true);
worker.start();
}
public void schedule(Runnable command, long delay, TimeUnit unit) {
queue.put(new ScheduledTask(command, delay, unit));
}
public void stop() {
running = false;
}
}
// 测试主类
public class SchedulerExample {
public static void main(String[] args) throws InterruptedException {
SimpleScheduler scheduler = new SimpleScheduler();
AtomicInteger counter = new AtomicInteger(0);
System.out.println("当前时间: " + System.currentTimeMillis());
scheduler.schedule(() -> System.out.println("Task 1 executed at: " + System.currentTimeMillis()), 2, TimeUnit.SECONDS);
scheduler.schedule(() -> System.out.println("Task 2 executed at: " + System.currentTimeMillis()), 5, TimeUnit.SECONDS);
Thread.sleep(6000);
scheduler.stop();
}
}
5.3 限时重试队列
java
import java.util.concurrent.*;
import java.util.Random;
// 定义重试任务
class RetryTask implements Delayed {
private final String taskId;
private final int retryCount;
private final long retryTime; // 下次重试时间点(纳秒)
private static final long BASE_DELAY_MS = 1000; // 基础延迟 1 秒
public RetryTask(String taskId, int retryCount) {
this.taskId = taskId;
this.retryCount = retryCount;
// 重试间隔随重试次数增加而指数退避
long delayMs = BASE_DELAY_MS * (1L << retryCount);
this.retryTime = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delayMs);
}
public String getTaskId() { return taskId; }
public int getRetryCount() { return retryCount; }
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(retryTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
if (o instanceof RetryTask) {
return Long.compare(this.retryTime, ((RetryTask) o).retryTime);
}
return 0;
}
}
// 重试处理器
class RetryHandler {
private final DelayQueue<RetryTask> queue = new DelayQueue<>();
private volatile boolean running = true;
private final Random random = new Random();
public RetryHandler() {
Thread worker = new Thread(() -> {
while (running) {
try {
RetryTask task = queue.take();
process(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
worker.setDaemon(true);
worker.start();
}
private void process(RetryTask task) {
System.out.printf("[%tT] 开始处理任务: %s, 重试次数: %d%n", System.currentTimeMillis(), task.getTaskId(), task.getRetryCount());
// 模拟处理,70% 概率失败
if (random.nextDouble() < 0.7 && task.getRetryCount() < 3) {
System.out.printf("[%tT] 任务 %s 处理失败,稍后重试%n", System.currentTimeMillis(), task.getTaskId());
queue.put(new RetryTask(task.getTaskId(), task.getRetryCount() + 1));
} else {
System.out.printf("[%tT] 任务 %s 处理成功或达到最大重试次数%n", System.currentTimeMillis(), task.getTaskId());
}
}
public void submit(String taskId) {
queue.put(new RetryTask(taskId, 0));
}
public void stop() {
running = false;
}
}
public class RetryExample {
public static void main(String[] args) throws InterruptedException {
RetryHandler handler = new RetryHandler();
handler.submit("Request-123");
Thread.sleep(20000);
handler.stop();
}
}
5.4 使用 drainTo 批量清理过期元素
java
import java.util.concurrent.*;
import java.util.ArrayList;
import java.util.List;
public class DrainToExample {
static class ExpiringItem implements Delayed {
private final String name;
private final long expireTime;
public ExpiringItem(String name, long delay, TimeUnit unit) {
this.name = name;
this.expireTime = System.nanoTime() + unit.toNanos(delay);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.expireTime, ((ExpiringItem) o).expireTime);
}
@Override
public String toString() {
return name;
}
}
public static void main(String[] args) throws InterruptedException {
DelayQueue<ExpiringItem> queue = new DelayQueue<>();
// 插入不同延时的元素
queue.put(new ExpiringItem("A-100ms", 100, TimeUnit.MILLISECONDS));
queue.put(new ExpiringItem("B-50ms", 50, TimeUnit.MILLISECONDS));
queue.put(new ExpiringItem("C-200ms", 200, TimeUnit.MILLISECONDS));
queue.put(new ExpiringItem("D-10ms", 10, TimeUnit.MILLISECONDS));
System.out.println("初始队列大小: " + queue.size()); // 4
// 等待一段时间,让部分元素过期
Thread.sleep(80);
List<ExpiringItem> expiredItems = new ArrayList<>();
// drainTo 只会转移已过期的元素
int drained = queue.drainTo(expiredItems);
System.out.println("转移了 " + drained + " 个过期元素: " + expiredItems);
System.out.println("剩余队列大小: " + queue.size()); // 未过期的 A 和 C
System.out.println("剩余元素: " + queue);
}
}
5.5 演示 Leader-Follower 效果
java
import java.util.concurrent.*;
public class LeaderFollowerDemo {
static class SimpleDelayed implements Delayed {
private final String id;
private final long triggerTime;
public SimpleDelayed(String id, long delay, TimeUnit unit) {
this.id = id;
this.triggerTime = System.nanoTime() + unit.toNanos(delay);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(triggerTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.triggerTime, ((SimpleDelayed) o).triggerTime);
}
@Override
public String toString() {
return id;
}
}
public static void main(String[] args) {
DelayQueue<SimpleDelayed> queue = new DelayQueue<>();
int consumerCount = 5;
// 启动多个消费者
for (int i = 0; i < consumerCount; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.printf("[%tT] 消费者 %d 启动,等待 take...%n", System.currentTimeMillis(), id);
SimpleDelayed item = queue.take();
System.out.printf("[%tT] 消费者 %d 取到元素: %s%n", System.currentTimeMillis(), id, item);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer-" + i).start();
}
// 生产者:先放入一个 3 秒后过期的元素,然后很快放入一个 0.5 秒后过期的元素
new Thread(() -> {
try {
Thread.sleep(100); // 确保消费者先启动
System.out.printf("[%tT] 生产者放入 LongDelay (3s)%n", System.currentTimeMillis());
queue.put(new SimpleDelayed("LongDelay", 3, TimeUnit.SECONDS));
Thread.sleep(200);
System.out.printf("[%tT] 生产者放入 ShortDelay (0.5s),它将成为新的堆顶%n", System.currentTimeMillis());
queue.put(new SimpleDelayed("ShortDelay", 500, TimeUnit.MILLISECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Producer").start();
// 运行观察:可以看到只有一个消费者(leader)会被唤醒并取出 ShortDelay,
// 然后 leader 会唤醒下一个消费者来处理 LongDelay,而不是所有 5 个消费者都被频繁唤醒。
}
}
预期输出分析:当 ShortDelay 插入时,由于它成为新的堆顶,offer 方法会执行 leader = null; available.signal();。这会唤醒一个正在等待的消费者(可能正在无限等待,或者之前已成为 leader 但等待时间更长),被唤醒的消费者重新检查堆顶,发现 ShortDelay 还有 0.5 秒,于是它成为新的 leader 进行 0.5 秒定时等待。其他消费者继续无限等待。0.5 秒后,该 leader 被唤醒,取出元素,并在 finally 中唤醒下一个 follower。整个过程清晰展示了链式唤醒机制。
6. 吞吐量与性能分析
6.1 单锁设计的影响
DelayQueue 仅使用一把 ReentrantLock 来保护内部 PriorityQueue。这与 PriorityBlockingQueue 的设计一致。
- 优点:实现简单,逻辑清晰,易于保证线程安全。
- 缺点 :在极高并发场景下,生产者和消费者会竞争同一把锁,可能成为吞吐量瓶颈。但由于
DelayQueue通常用于延时任务处理,其并发量往往不如常规的生产者-消费者队列(如LinkedBlockingQueue),单锁带来的开销通常可以接受。 - 生产者 :
offer操作通常非常快(堆上浮 O(log n)),持锁时间极短。 - 消费者 :
take和poll操作虽然可能涉及定时等待,但在等待期间锁是释放的,只有在检查状态、修改堆和唤醒其他线程时才持有锁。因此,消费者之间并不会因为长时间的锁占用而互相阻塞。
6.2 Leader-Follower 模式的性能优势
这是 DelayQueue 最重要的性能优化手段。
- 避免了惊群效应 :在没有此优化的设计中,N 个消费者线程会在队首元素过期时被同时唤醒,产生 N-1 次无效的锁竞争和线程上下文切换。Leader-Follower 模式将每次过期事件触发的唤醒次数降低到了常数级别(约 1-2 次)。
- 适用场景 :当消费者线程数量较多时,这种优化效果极其显著。如果只有一个消费者线程,
leader机制几乎不产生额外开销。
6.3 堆调整开销
- 插入 (
offer):时间复杂度为 O(log n)。对于大规模延时任务(例如上百万个定时任务),堆的深度增加,每次插入操作需要进行更多次比较和交换,CPU 开销会随之上升。 - 删除 (
take/poll):时间复杂度同样为 O(log n)。 - 结论 :
DelayQueue不适合存储海量(百万级以上)的延迟元素。如果确实有此需求,可能需要考虑使用时间轮(Hashed Wheel Timer)等更复杂的算法(如 Netty 的HashedWheelTimer)。
6.4 内存占用
DelayQueue 内部使用 PriorityQueue,其底层是 Object[] 数组。没有像 LinkedBlockingQueue 那样的 Node 节点对象开销,因此内存利用率较高。但元素对象本身必须包含用于计算过期时间的字段(如 expireTime),这会增加一些内存开销。
6.5 对比其他队列的吞吐量
LinkedBlockingQueue:在纯 FIFO 场景下,由于使用了双锁(putLock 和 takeLock),生产者与消费者之间的并发度更高,吞吐量通常优于DelayQueue。ArrayBlockingQueue:单锁设计,与DelayQueue类似,但其元素存取不涉及堆调整,开销更小。但其有界性限制了生产速率。ScheduledThreadPoolExecutor:JDK 的定时任务线程池内部正是使用了DelayQueue的变体(DelayedWorkQueue)来管理定时任务。DelayedWorkQueue针对ScheduledFutureTask做了特定优化,但核心原理与DelayQueue一致。
6.6 性能调优建议
- 合理预估容量 :虽然
DelayQueue是无界的,但可以通过带有集合参数的构造器来初始化PriorityQueue,给予一个合适的初始容量(new DelayQueue(new ArrayList<>(initialCapacity))),避免数组频繁扩容带来的开销。 - 批量处理过期元素 :优先使用
drainTo方法而非循环调用poll()。drainTo在单次锁持有期间内批量转移元素,减少了锁获取和释放的次数。 - 控制消费者数量:鉴于 Leader-Follower 模式的优化,适量的消费者线程即可满足处理需求,过多的消费者线程反而会增加线程调度的负担。
- 监控队列大小 :由于队列无界,务必监控
queue.size(),防止因生产者速度远大于消费者处理速度导致内存溢出(OOM)。
7. 注意事项与常见陷阱
- 元素必须正确实现
compareTo:compareTo方法的逻辑必须 与getDelay方法保持一致,即按照剩余延迟时间升序排列。如果compareTo实现错误(例如固定返回 0 或按插入顺序),会导致堆结构混乱,take可能永远无法取出正确的过期元素。
getDelay的时间单位 :getDelay(TimeUnit unit)接收一个时间单位参数,必须使用该参数进行转换并返回正确的数值。直接返回毫秒值或纳秒值而忽略unit参数是常见错误。JDK 内部通常使用NANOSECONDS进行计算。
take的阻塞性与队列非空的关系 :- 新手容易误认为只要队列不为空(
size() > 0),take就不会阻塞。实际上,即使队列中有成千上万个元素,只要堆顶的那个元素未过期,take就会一直阻塞。这是DelayQueue与其他阻塞队列最本质的区别。
- 新手容易误认为只要队列不为空(
peek()不检查过期 :peek()方法仅仅是查看堆顶元素,无论该元素是否过期。不能依赖peek() != null来判断是否有可用元素,必须结合getDelay()或直接使用poll()。
size()包括未过期元素 :size()返回的是队列中所有元素的总数,包括那些还未到期的元素。因此,size() > 0并不意味着poll()会返回非空值。
- 无界导致内存溢出 :
offer和put永不阻塞。如果消费者线程处理缓慢或崩溃,而生产者持续添加延时较长的元素,队列将无限制增长,最终耗尽 JVM 堆内存。生产环境中务必结合监控和限流策略。
- Leader-Follower 模式的公平性问题 :
DelayQueue本身不保证线程调度的公平性(非公平锁)。如果不断有剩余延迟更短的元素插入,会导致原本等待较长延迟元素的leader线程被反复重置和重新等待,极端情况下可能造成某些等待线程饥饿。不过,DelayQueue关注的是时间优先 而非线程公平。
- 中断处理 :
take()和poll(long, TimeUnit)方法会抛出InterruptedException。捕获该异常时,通常应恢复中断状态(Thread.currentThread().interrupt())并退出处理循环,以便上层调用者感知到中断。
drainTo只转移已过期元素 :- 使用
drainTo时需注意,它可能不会转移任何元素(如果所有元素均未过期),也可能只转移部分元素(达到最大数量限制前就遇到了未过期元素)。剩余未过期的元素会继续留在队列中。
- 使用
- 元素可变性问题 :
- 元素在放入
DelayQueue后,其决定过期时间的字段(如expireTime)不应被修改 。一旦修改,PriorityQueue无法感知这种变化,堆序将被破坏,导致不可预知的行为。
- 元素在放入
8. 与其他阻塞队列的对比总结
| 特性维度 | DelayQueue | PriorityBlockingQueue | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|---|---|
| 底层容器 | PriorityQueue (二叉堆) |
PriorityQueue (二叉堆) |
数组 (Object[]) |
单向链表 (Node) |
| 有界性 | 无界 (Integer.MAX_VALUE) |
无界 (Integer.MAX_VALUE) |
有界 (构造时指定) | 可选有界 (默认无界) |
| 锁数量 | 单锁 (ReentrantLock) |
单锁 (ReentrantLock) |
单锁 (ReentrantLock) |
双锁 (putLock, takeLock) |
| 是否支持优先级 | 是 (基于剩余延时) | 是 (基于 Comparable 或 Comparator) |
否 (严格 FIFO) | 否 (严格 FIFO) |
| 生产者阻塞条件 | 永不阻塞 (无界) | 永不阻塞 (无界,但扩容可能耗时) | 队列满时阻塞 | 队列满时阻塞 (有界模式下) |
| 消费者阻塞条件 | 队列空 或 队首元素未过期 | 队列空 | 队列空 | 队列空 |
| 特殊优化 | Leader-Follower 模式 避免惊群 | 扩容时的自旋 CAS 优化 | 数组循环利用 | 双锁分离 提高并发吞吐量 |
| 典型应用场景 | 延时任务、缓存过期、定时重试 | 优先级任务调度、VIP 请求处理 | 固定大小的缓冲区、连接池 | 高吞吐量的生产者-消费者模型、日志异步处理 |
9. 总结与学习指引
DelayQueue 是 JUC 包中一个功能独特且设计精巧的并发组件。它巧妙地在 PriorityQueue 的基础上叠加了时间维度的检查,并通过 Leader-Follower 模式解决了多消费者并发等待时的性能痛点,使得延时任务的处理变得高效且易于使用。
核心要点回顾:
- 无界、延时、优先级是其三大标签。
PriorityQueue负责维护元素的到期顺序。- 单锁保证线程安全,实现简洁。
leader线程是性能优化的关键,避免了惊群效应。- 生产者永不阻塞,需警惕 OOM 风险。
使用建议:
- 当你需要"在未来某个时间点处理某个任务"时,
DelayQueue是首选的并发工具之一。 - 务必小心实现
Delayed接口的compareTo和getDelay方法。 - 养成使用
drainTo批量处理过期元素的习惯。 - 在生产环境部署时,务必监控队列的
size,并考虑使用有界策略进行自我保护。 - 如果你的场景是高吞吐量的实时数据处理,且不关心延迟和优先级,
LinkedBlockingQueue会是更优的选择。