DelayQueue

延迟队列,顾名思义,是一个可延迟的队列,这是一个无界队列,其最大的特点就是队列中的数据按照设定的等待时间长短排序,队列的头部是剩余时间最短的元素。内部组合了PriorityQueue优先队列,队列的操作都是依靠该队列完成的;延迟队列的元素必须是Delayed的子类,而Delayed是Comparable的子接口。Delayed接口中的getDelay方法获取某个元素的剩余时间,只有剩余时间为0才能从队列头中获取元素

成员变量

// 独占锁
private final transient ReentrantLock lock = new ReentrantLock();
// 优先级队列
private final PriorityQueue<E> q = new PriorityQueue<E>();

// 如果队列中存在元素,那么第一个调用take方法的线程会成为leader,它将会在available等待队列上等待头结点剩余的延迟时间,其余线程将会成为follower线程,它们会一直在available上等待
// leader线程被唤醒之后会将leader置为null,在获取到元素之后会唤醒一个在available上等待的follower线程,这个被唤醒的follower线程可能成为新的leader线程
private Thread leader = null;

// 条件等待
private final Condition available = lock.newCondition();

offer

将元素添加到延迟队列中

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();
    }
}
  • 获取ReentrantLock锁并加锁
  • 将元素添加到PriorityQueue队列中,添加的元素需要实现compareTo方法,该方法应该使用时间进行比较,这样才能符合DelayQueue的性质
  • 从PriorityQueue队列中获取一个元素但不删除该元素,如果获取的元素和新添加的元素相等,那么说明该元素就是时间最短的元素,将leader置为null并唤醒available上的一个线程和该线程竞争leader
  • 释放锁

add(E e)、put(E e)、offer(E e, long timeout, TimeUnit unit)这三个方法底层都是调用的offer(E e)

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();
    }
}
  • 从PriorityBlockingQueue中获取元素first但不删除
  • 如果first为null,说明该队列为null,则该线程在available上等待
  • 否则first不为null,调用该元素对应的getDelay方法获取剩余的延迟时间delay
    • 如果delay小于等于0,说明该元素已经可以出队,从PriorityBlockingQueue队列中取出该元素并删除
    • 如果delay大于等于0,说明该元素还未到延迟释放时间,将first置为null,因为接下来该线程会将入等待,等待完毕之后会重新循环获取队列头部(如果不置为null,可能造成内存泄漏,因为first指向的头结点可能被多个线程内部的first变量同时持有,如果在线程等待之前不将first置为null,那么如果某个时刻该节点出队列,这个节点应该被清楚,但由于还有别的等待线程持有该节点对象的引用,导致该节点对象不能被及时清理,造成内存泄漏)
    • 如果leader不为null,说明有别的线程进入了超时等待,而上面的情况说明有一个leader线程在等待延迟时间最短的元素出队列,那么该线程自然成为follower线程(这里leader不为null,说明没有新元素入队或者有新元素入队但该元素不是延迟时间最短的元素)
    • 如果leader为null,获取当前线程thisThread,将当前线程置为leader线程,让当前线程在available上等待delay纳秒,直到被唤醒,然后进入finally代码块,判断leader是否和thisThread相等,如果相等将leader置为null
  • 进入外层finally代码块,如果leader为null并且从PriorityBlockingQueue队列中获取的元素不为null,唤醒一个available上等待的消费线程,让它和新消费线程重新争夺leader

poll

该方法也是从延迟队列中获取元素,和take方法不同之处在于该方法不支持中断,并且如果获取不到元素返回null,不会在available上等待

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();
    }
}
  • 获取ReentrantLock锁,并加锁
  • 从PriorityBlockingQueue中获取元素first但不删除该元素
  • 如果first为null或者该元素的延迟时间大于0,说明队列为空或者该元素还不到延迟时间,无法从队列中出队,则返回null
  • 否则将该元素从PriorityBlockingQueue队列中取出并删除
  • 释放锁
posted @ 2021-07-17 11:11  扭不动的奥利奥  阅读(126)  评论(0)    收藏  举报