DelayQueue 源码深度分析
DelayQueue 是 Java 并发包(java.util.concurrent)提供的阻塞延迟队列,核心特性是:队列中的元素必须实现 Delayed 接口,只有当元素的「延迟时间到期」后,才能被取出。它底层基于优先级队列(PriorityQueue)实现排序,通过重入锁(ReentrantLock)和条件变量(Condition)保证线程安全与阻塞特性,适用于定时任务调度、缓存过期清理等场景。
一、核心定位与类结构
1. 类定义与继承关系
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
// ... 核心实现
}
- 泛型约束:元素必须实现
Delayed接口(强制要求元素具备「延迟属性」)。 - 接口实现:实现
BlockingQueue,支持阻塞式入队 / 出队(put/take)、带超时的入队 / 出队(offer(timeout)/poll(timeout))。 - 父类:继承
AbstractQueue,复用队列基础骨架方法(如add本质调用offer)。
2. 核心成员变量
DelayQueue 的核心依赖 3 个关键组件,全部被 transient 修饰(避免序列化时破坏线程安全):
// 1. 独占锁:保证所有入队/出队操作的原子性,线程安全的核心
private final transient ReentrantLock lock = new ReentrantLock();
// 2. 底层存储:基于优先级队列,按元素「剩余延迟时间」升序排序(队首是最早到期的元素)
private final PriorityQueue<E> q = new PriorityQueue<>();
// 3. 条件变量:优化等待逻辑(leader 线程机制)
// - leader:当前正在等待「队首元素到期」的线程(减少不必要的超时等待)
private transient Thread leader = null;
// - available:用于唤醒等待「元素可用」(队列非空且元素到期)的线程
private final Condition available = lock.newCondition();
3. Delayed 接口(延迟元素的核心契约)
元素必须实现 Delayed 接口,定义了两个核心方法:
public interface Delayed extends Comparable<Delayed> {
// 1. 返回元素的「剩余延迟时间」(单位:纳秒),<=0 表示已到期
long getDelay(TimeUnit unit);
// 2. 继承 Comparable:用于 PriorityQueue 排序(按延迟时间升序)
int compareTo(Delayed o);
}
- 核心要求:
compareTo必须与getDelay逻辑一致(例如:剩余延迟时间短的元素,compareTo返回负数,确保队首是最早到期的元素)。
二、构造方法
DelayQueue 仅提供两个构造方法,本质都是初始化底层 PriorityQueue:
// 1. 无参构造:初始化空的优先级队列(默认容量 11)
public DelayQueue() {}
// 2. 带集合的构造:将集合元素初始化到优先级队列
public DelayQueue(Collection<? extends E> c) {
this.addAll(c); // 底层调用 PriorityQueue.addAll(),会触发元素排序
}
三、核心方法源码分析
DelayQueue 的核心逻辑集中在「入队」和「出队」,下面逐一拆解关键方法。
1. 入队方法:offer (E e)
offer 是无阻塞入队(DelayQueue 是无界队列,入队永远成功),核心逻辑:加锁 → 插入优先级队列 → 唤醒等待线程(若插入的是队首)→ 解锁。
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 加独占锁,保证原子性
try {
q.offer(e); // 插入 PriorityQueue,按延迟时间排序
// 若插入的元素是队首(最早到期),说明之前可能无可用元素,唤醒等待线程
if (q.peek() == e) {
leader = null; // 重置 leader(新队首需要重新确定等待线程)
available.signal(); // 唤醒 available 上的一个等待线程
}
return true; // 无界队列,永远返回 true
} finally {
lock.unlock(); // 解锁(finally 保证锁一定释放)
}
}
- 关键细节:插入元素后,若元素成为队首,必须唤醒等待线程(因为之前可能有线程在等「元素可用」),避免线程永久阻塞。
- 其他入队方法:
add(E e):继承自AbstractQueue,本质调用offer(e)(无阻塞,失败抛异常,但 DelayQueue 无界,永远成功)。put(E e):实现BlockingQueue,因队列无界,直接调用offer(e),无阻塞。
2. 出队方法:take ()(阻塞获取,直到元素到期)
take 是阻塞式出队:若队列空或队首元素未到期,线程会阻塞,直到有元素到期或被中断。核心逻辑依赖「leader 线程优化」(减少不必要的超时等待)。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 可中断加锁(支持线程中断响应)
try {
for (;;) { // 循环检查(避免虚假唤醒)
E first = q.peek(); // 查看队首元素(不删除)
if (first == null) {
// 队列空:阻塞在 available 条件变量上,等待入队线程唤醒
available.await();
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS); // 计算剩余延迟时间
if (delay <= 0) {
// 元素已到期:从 PriorityQueue 中移除并返回
return q.poll();
}
// 元素未到期:释放队首元素引用(避免内存泄漏)
first = null;
if (leader != null) {
// 已有 leader 线程在等待队首到期:当前线程阻塞,等待唤醒
available.await();
} else {
// 无 leader 线程:当前线程成为 leader,超时等待 delay 时间
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 阻塞 delay 纳秒(到期自动唤醒,或被入队线程唤醒)
available.awaitNanos(delay);
} finally {
// 唤醒后重置 leader(避免后续线程无法成为 leader)
if (leader == thisThread) {
leader = null;
}
}
}
}
}
} finally {
// 出队后,若队列非空且无 leader,唤醒下一个等待线程
if (leader == null && q.peek() != null) {
available.signal();
}
lock.unlock(); // 解锁
}
}
核心设计:leader 线程优化
- 问题背景:若多个线程同时调用
take,若队首元素未到期,所有线程都超时等待会造成资源浪费。 - 优化逻辑:
- 仅允许一个「leader 线程」超时等待队首元素到期。
- 其他线程直接阻塞在
available上,等待 leader 唤醒或新元素入队唤醒。 - leader 被唤醒后(元素到期或超时),重置 leader,让后续线程竞争成为新 leader。
- 效果:减少不必要的超时等待和上下文切换,提升性能。
3. 出队方法:poll ()(非阻塞 / 带超时)
(1)无参 poll ():非阻塞,立即返回
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
// 队首为空 或 未到期 → 返回 null
if (first == null || first.getDelay(TimeUnit.NANOSECONDS) > 0) {
return null;
} else {
// 元素到期 → 出队
return q.poll();
}
} finally {
lock.unlock();
}
}
(2)带超时 poll (long timeout, TimeUnit unit):阻塞指定时间
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) {
if (nanos <= 0) {
return null; // 超时且队列空 → 返回 null
} else {
// 阻塞 nanos 纳秒,返回剩余未阻塞时间
nanos = available.awaitNanos(nanos);
}
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay <= 0) {
return q.poll(); // 元素到期 → 出队
}
if (nanos <= 0) {
return null; // 超时且元素未到期 → 返回 null
}
first = null; // 释放引用,避免内存泄漏
// 若剩余超时时间 < 元素延迟时间,直接阻塞剩余时间(无需等元素到期)
if (nanos < delay || leader != null) {
nanos = available.awaitNanos(nanos);
} else {
// 成为 leader,阻塞 delay 时间(元素到期)
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
long timeLeft = available.awaitNanos(delay);
nanos -= delay - timeLeft; // 更新剩余超时时间
} finally {
if (leader == thisThread) {
leader = null;
}
}
}
}
}
} finally {
if (leader == null && q.peek() != null) {
available.signal();
}
lock.unlock();
}
}
- 核心逻辑:结合「超时等待」和「leader 优化」,若剩余超时时间不足以等待元素到期,直接阻塞剩余时间,避免无效等待。
4. 其他关键方法
(1)peek ():查看队首元素(不删除,非阻塞)
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.peek(); // 直接返回 PriorityQueue 的队首
} finally {
lock.unlock();
}
}
- 注意:返回的元素可能未到期,仅用于查看,不能直接使用。
(2)size ():获取队列元素个数
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.size(); // 依赖 PriorityQueue 的 size
} finally {
lock.unlock();
}
}
(3)clear ():清空队列
public void clear() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.clear(); // 清空 PriorityQueue
} finally {
lock.unlock();
}
}
四、核心原理总结
1. 延迟机制实现
- 元素约束:元素必须实现
Delayed接口,通过getDelay()提供剩余延迟时间,compareTo()保证排序。 - 排序核心:底层
PriorityQueue按「剩余延迟时间」升序排序,队首永远是最早到期的元素。 - 到期判断:出队时(
take/poll)通过getDelay(TimeUnit.NANOSECONDS) <= 0判断元素是否到期。
2. 线程安全与阻塞实现
- 锁机制:所有操作(入队 / 出队 / 查看 / 清空)都通过
ReentrantLock加锁,保证原子性和线程安全。 - 条件变量:available 条件变量用于线程阻塞 / 唤醒:
- 入队时:若插入元素是队首,唤醒
available上的等待线程(可能有线程在等元素可用)。 - 出队时:队列空或元素未到期,线程阻塞在
available上。
- 入队时:若插入元素是队首,唤醒
- leader 优化:减少多线程竞争时的无效超时等待,提升并发性能。
3. 无界特性
DelayQueue 是无界队列(底层 PriorityQueue 无界),因此:
offer()/add()/put()永远不会阻塞,也不会返回false(put无阻塞是因为无界,不会满)。- 若元素添加过快,可能导致内存溢出(OOM),需注意控制元素数量。
五、注意事项与使用场景
1. 关键注意事项
Delayed接口实现一致性:compareTo必须与 getDelay逻辑一致,否则排序错误,导致元素无法按时出队。- 错误示例:
compareTo按元素 ID 排序,getDelay按时间排序 → 队首可能是未到期元素,到期元素被压在队列中。
- 错误示例:
- 内存泄漏风险:
take/poll方法中会主动释放队首元素引用(first = null),避免线程持有元素引用导致 GC 无法回收。 - 线程中断:
take()/poll(timeout)支持线程中断(lockInterruptibly()),中断后会抛出InterruptedException,需处理。
2. 典型使用场景
- 定时任务调度:例如实现简单的定时任务框架,队列中存储任务,线程通过
take阻塞获取到期任务并执行。 - 缓存过期清理:缓存元素存入队列,到期后自动出队,触发清理逻辑。
- 延迟通知:例如订单创建后 30 分钟未支付,自动取消,队列中存储订单 ID 和延迟时间,到期后取出处理。
3. 示例代码
// 1. 实现 Delayed 接口的元素类
class DelayTask implements Delayed {
private final String taskName;
private final long expireTime; // 过期时间(毫秒时间戳)
public DelayTask(String taskName, long delayMs) {
this.taskName = taskName;
this.expireTime = System.currentTimeMillis() + delayMs;
}
@Override
public long getDelay(TimeUnit unit) {
// 计算剩余延迟时间(纳秒)
long remaining = expireTime - System.currentTimeMillis();
return unit.convert(remaining, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
// 按过期时间升序排序(早到期的在前)
return Long.compare(this.expireTime, ((DelayTask) o).expireTime);
}
@Override
public String toString() {
return "DelayTask{" + "taskName='" + taskName + "'}";
}
}
// 2. 测试 DelayQueue
public class DelayQueueDemo {
public static void main(String[] args) throws InterruptedException {
DelayQueue<DelayTask> queue = new DelayQueue<>();
// 入队:3 个延迟任务(延迟 1s、2s、3s)
queue.offer(new DelayTask("任务1", 1000));
queue.offer(new DelayTask("任务2", 2000));
queue.offer(new DelayTask("任务3", 3000));
// 出队:阻塞获取到期任务
for (int i = 0; i < 3; i++) {
DelayTask task = queue.take(); // 阻塞直到任务到期
System.out.println("执行任务:" + task + ",时间:" + System.currentTimeMillis());
}
}
}
-
输出结果(任务按延迟时间依次执行):
执行任务:DelayTask{taskName='任务1'},时间:1730200000000 执行任务:DelayTask{taskName='任务2'},时间:1730200001000 执行任务:DelayTask{taskName='任务3'},时间:1730200002000
六、核心总结
DelayQueue 的底层设计可概括为「优先级队列 + 锁 + 条件变量 + 延迟接口」:
- 排序核心:
PriorityQueue按延迟时间升序排序,保证队首是最早到期元素。 - 延迟判断:
Delayed接口定义延迟属性,出队时校验到期状态。 - 线程安全:
ReentrantLock保证操作原子性,避免并发问题。 - 阻塞优化:
Condition+leader线程机制,减少无效等待,提升并发性能。
它是 Java 中实现「延迟任务」的经典工具,理解其源码设计(尤其是 leader 优化和阻塞逻辑),能帮助我们更合理地使用它,避免踩坑。

224

被折叠的 条评论
为什么被折叠?



