DelayQueue 源码深度分析

AI的出现,是否能替代IT从业者? 10w+人浏览 1.5k人参与

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,若队首元素未到期,所有线程都超时等待会造成资源浪费。
  • 优化逻辑:
    1. 仅允许一个「leader 线程」超时等待队首元素到期。
    2. 其他线程直接阻塞在 available 上,等待 leader 唤醒或新元素入队唤醒。
    3. 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() 永远不会阻塞,也不会返回 falseput 无阻塞是因为无界,不会满)。
  • 若元素添加过快,可能导致内存溢出(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 的底层设计可概括为「优先级队列 + 锁 + 条件变量 + 延迟接口」:

  1. 排序核心PriorityQueue 按延迟时间升序排序,保证队首是最早到期元素。
  2. 延迟判断Delayed 接口定义延迟属性,出队时校验到期状态。
  3. 线程安全ReentrantLock 保证操作原子性,避免并发问题。
  4. 阻塞优化Condition + leader 线程机制,减少无效等待,提升并发性能。

它是 Java 中实现「延迟任务」的经典工具,理解其源码设计(尤其是 leader 优化和阻塞逻辑),能帮助我们更合理地使用它,避免踩坑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值