Java队列之DelayQueue源码解析

目录

1.DelayQueue

1.1整体结构

1.2放数据

1.3拿数据


1.DelayQueue


1.1整体结构

  • 延迟执行,并且可以设置延迟多久之后执行
  • 队列中元素将在过期时被执行,越靠近队头,越早过期
  • 底层使用了优先级队列来实现,复用组合了PriorityQueue(策略者模式),让先过期的元素先执行
//队列的元素必须为Delayed本身或者子类
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {
    ...
    private final PriorityQueue<E> q = new PriorityQueue<E>();
    // 只有一把锁,读和写同一时刻只能有一个在运行
    private transient final ReentrantLock lock = new ReentrantLock();
    ...
}
//继承了Comparable接口,用来比较大小
public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}
  • DelayQueue 队列中的元素必须是实现 Delayed 接口和 Comparable 接口的,并覆写了 getDelay 方法和 compareTo 的方法才行,不然在编译时,编译器就会提醒我们元素必须强制实现 Delayed 接口。

ps:只有一把锁,读和写同一时刻只能有一个在运行

1.2放数据

public boolean offer(E e) {
    final ReentrantLock lock = this.lock; //会锁住当前整个实例
    // 上锁
    lock.lock();
    try {
        // 使用 PriorityQueue 的扩容,排序
        q.offer(e);
        // 如果恰好刚放进去的元素正好在队列头,需要唤醒 take 的阻塞线程
        // 空队列一定也会进入判断,因为一定是队列头!!!
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

DelayQueue的offer方法核心块使用了PriorityQueue的offer方法

加锁,使用PriorityQueue的offer方法判断扩容并且按序插入,如果元素恰好放在队列头就唤醒一个take线程

// PriorityQueue的offer方法:新增元素
public boolean offer(E e) {
    // 如果是空元素的话,抛异常
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    // 队列实际大小大于容量时,进行扩容
    // 扩容策略是:如果老容量小于 64,2 倍扩容,如果大于 64,50 % 扩容
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    // 如果队列为空,当前元素正好处于队头
    if (i == 0)
        queue[0] = e;
    else
    // 如果队列不为空,插入堆,从下向上冒泡
        siftUp(i, e);
    return true;
}
// 维护一个最小堆!!!
// 新增操作:从下向上冒泡的过程不需要左右节点在比较的!!!
// 删除操作:从上向下冒泡的过程是需要左右节点比较然后在进行交换的!!!
    private void siftUp(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        // k 是当前队列实际大小的位置
        while (k > 0) {
            // 对 k 进行减倍
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            // 如果 x 比 e 大,退出,把 x 放在 k 位置上
            if (key.compareTo((E) e) >= 0)
                break;
            // x 比 e 小,继续循环,直到找到 x 比队列中元素大的位置
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

PriorityQueue的offer方法:先判断是否需要扩容(如果元素个数少于64每次扩容2倍,否则每次扩容1.5倍),如果队列为空那么直接在头节点新增即可,如果不为空,就需要进行堆插入从下向上进行冒泡,不断的跟父节点进行比较即可,最后返回true新增成功!

ps:因为本身就是一个堆,所以不需要在判断另一个节点的值,这一点跟从上向下冒泡不同,后者需要比较出左右节点的最值!

ps:堆排序首先需要维护一个堆(从下向上对每个非叶子节点自上向下冒泡维护一个最大堆),然后进行实际的排序(每次与堆尾部交换头节点,然后从堆头自上向下维护堆结构)

1.3拿数据

//take方法:无限阻塞
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock; //获取锁,会锁住整个实例
    lock.lockInterruptibly();// 可中断锁
    for (;;) {
        // 从队头中拿数据出来
        E first = q.peek();
        // 如果为空,说明队列中,没有数据,阻塞住
        if (first == null)
            available.await();
        else {
            // 获取队头数据的过期时间
            long delay = first.getDelay(NANOSECONDS);
            // 如果过期了,直接返回队头数据
            if (delay <= 0)
                return q.poll();
            // 引用置为 null ,便于 gc,这样可以让线程等待时,回收 first 变量
            first = null;
            // leader 不为空的话,表示当前队列元素之前已经被设置过阻塞时间了
            // 已经被设置阻塞时间不会进入这里吧!!!!finally不是保证重置了吗???
            if (leader != null)
                available.await();
            else {
              // 之前没有设置过阻塞时间,按照一定的时间进行阻塞
                Thread thisThread = Thread.currentThread();
                leader = thisThread;
                try {
                    // 进行阻塞,等待delay延迟时间
                    available.awaitNanos(delay);
                } finally {//一定会重置
                    if (leader == thisThread)
                        leader = null;
                }
            }
    }finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
    }
}

加锁并且设置为可中断,如果队列为空就会阻塞,如果不为空会先获取延迟时间,如果延时过期(可以执行了)就删除头节点并且执行,如果没有设置阻塞时间会进行延时等待

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章