* DelayedWorkQueue: 定时调度延时队列

* 一种无界延迟阻塞队列, 它主要用于线程池定时或周期任务的使用

* ScheduledThreadPoolExecutor的内部类

* 存储结构是一个RunableScheduledFuture数组,平衡二叉堆以数组方式实现,根节点出队 元素实现了Delayed, Future接口.

* 用自己的方式实现了Delayed获取延时节点,和优先级队列.

  • DelayedWorkQueue的数据结构是基于堆实现的

  • DelayedWorkQueue采用数组实现堆,根节点出队,用最后叶子节点替换,然后下推至满足堆成立条件

  • 最后叶子节点入队,然后向上推至满足堆成立条件

  • DelayedWorkQueue取根节点延迟时间,然后等待,直到延迟结束,从新取根节点达到延迟取元素

堆是一个二叉树,但是它最简单的方式是通过数组去实现二叉树,而且因为堆是一个完全二叉树,就不存在数组空间的浪费。怎么使用数组来存储二叉树呢?

就是用数组的下标来模拟二叉树的各个节点 ,比如说根节点就是0,第一层的左节点是1,右节点是2。由此我们可以得出下列公式:

// 对于n位置的节点来说:
int left = 2 * n + 1; // 左子节点
int right = 2 * n + 2; // 右子节点
int parent = (n - 1) / 2; // 父节点,当然n要大于0,根节点是没有父节点的

数据结构

// 初始容量,当堆大小一溢出时,扩容按照1.5倍进行扩容
private static final int INITIAL_CAPACITY = 16;
// 堆存储数组,我们可以看到这里是通过数据实现对堆的存储,如果父节点下标为n,则左子节点下标为2*n+1, 又子节点是2*n+2
// 例如下标0的子节点分别为1和2
private RunnableScheduledFuture<?>[] queue = new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
// 全局锁,用来对操作加锁
private final ReentrantLock lock = new ReentrantLock();
// 初始化堆大小,有别于堆容量,size小于等于queue.length
private int size = 0;
// 获取堆头结点的线程
private Thread leader = null;
// 维护一个等待条件,当新的节点变成头节点时,或一个新线程成为leader时,进行singal
private final Condition available = lock.newCondition();

DelayedWorkQueue是用数组来储存队列中的元素,那么我们看看它是怎么实现优先级队列的。

插入元素排序siftUp方法

private void siftUp(int k, RunnableScheduledFuture<?> key) {
    // 当k==0时,就到了堆二叉树的根节点了,跳出循环
    while (k > 0) {
        // 父节点位置坐标, 相当于(k - 1) / 2
        int parent = (k - 1) >>> 1;
        // 获取父节点位置元素
        RunnableScheduledFuture<?> e = queue[parent];
        // 如果key元素大于父节点位置元素,满足条件,那么跳出循环
        // 因为是从小到大排序的。
        if (key.compareTo(e) >= 0)
            break;
        // 否则就将父节点元素存放到k位置
        queue[k] = e;
        // 这个只有当元素是ScheduledFutureTask对象实例才有用,用来快速取消任务。
        setIndex(e, k);
        // 重新赋值k,寻找元素key应该插入到堆二叉树的那个节点
        k = parent;
    }
    // 循环结束,k就是元素key应该插入的节点位置
    queue[k] = key;
    setIndex(key, k);
}

通过循环,来查找元素key应该插入在堆二叉树那个节点位置,并交互父节点的位置。具体流程在前面已经介绍过了。

移除元素排序siftDown方法

private void siftDown(int k, RunnableScheduledFuture<?> key) {
    int half = size >>> 1;
    // 通过循环,保证父节点的值不能小于子节点。
    while (k < half) {
        // 左子节点, 相当于 (k * 2) + 1
        int child = (k << 1) + 1;
        // 左子节点位置元素
        RunnableScheduledFuture<?> c = queue[child];
        // 右子节点, 相当于 (k * 2) + 2
        int right = child + 1;
        // 如果左子节点元素值大于右子节点元素值,那么右子节点才是较小值的子节点。
        // 就要将c与child值重新赋值
        if (right < size && c.compareTo(queue[right]) > 0)
            c = queue[child = right];
        // 如果父节点元素值小于较小的子节点元素值,那么就跳出循环
        if (key.compareTo(c) <= 0)
            break;
        // 否则,父节点元素就要和子节点进行交换
        queue[k] = c;
        setIndex(c, k);
        k = child;
    }
    queue[k] = key;
    setIndex(key, k);
}

通过循环,保证父节点的值不能小于子节点。

插入元素方法

public void put(Runnable e) {
    offer(e);
}
 
public boolean add(Runnable e) {
    return offer(e);
}
 
public boolean offer(Runnable e, long timeout, TimeUnit unit) {
    return offer(e);
}

我们发现与普通阻塞队列相比,这三个添加方法都是调用offer方法。那是因为它没有队列已满的条件,也就是说可以不断地向DelayedWorkQueue添加元素,当元素个数超过数组长度时,会进行数组扩容。

public boolean offer(Runnable x) {
    if (x == null)
        throw new NullPointerException();
    RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
    // 使用lock保证并发操作安全
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int i = size;
        // 如果要超过数组长度,就要进行数组扩容
        if (i >= queue.length)
            // 数组扩容
            grow();
        // 将队列中元素个数加一
        size = i + 1;
        // 如果是第一个元素,那么就不需要排序,直接赋值就行了
        if (i == 0) {
            queue[0] = e;
            setIndex(e, 0);
        } else {
            // 调用siftUp方法,使插入的元素变得有序。
            siftUp(i, e);
        }
        // 表示新插入的元素是队列头,更换了队列头,
        // 那么就要唤醒正在等待获取任务的线程。
        if (queue[0] == e) {
            leader = null;
            // 唤醒正在等待等待获取任务的线程
            available.signal();
        }
    } finally {
        lock.unlock();
    }
    return true;
}

主要是三步:

  1. 元素个数超过数组长度,就会调用grow()方法,进行数组扩容。

  2. 将新元素e添加到优先级队列中对应的位置,通过siftUp方法,保证按照元素的优先级排序。

  3. 如果新插入的元素是队列头,即更换了队列头,那么就要唤醒正在等待获取任务的线程。这些线程可能是因为原队列头元素的延时时间没到,而等待的

数组扩容方法

private void grow() {
    int oldCapacity = queue.length;
    // 每次扩容增加原来数组的一半数量。
    int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
    if (newCapacity < 0) // overflow
        newCapacity = Integer.MAX_VALUE;
    // 使用Arrays.copyOf来复制一个新数组
    queue = Arrays.copyOf(queue, newCapacity);
}

立即获取队列头元素

public RunnableScheduledFuture<?> poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        RunnableScheduledFuture<?> first = queue[0];
        // 队列头任务是null,或者任务延时时间没有到,都返回null
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            // 移除队列头元素
            return finishPoll(first);
    } finally {
        lock.unlock();
    }
}

当队列头任务是null,或者任务延时时间没有到,表示这个任务还不能返回,因此直接返回null。否则调用finishPoll方法,移除队列头元素并返回。

// 移除队列头元素
private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
    // 将队列中元素个数减一
    int s = --size;
    // 获取队列末尾元素x
    RunnableScheduledFuture<?> x = queue[s];
    // 原队列末尾元素设置为null
    queue[s] = null;
    if (s != 0)
        // 因为移除了队列头元素,所以进行重新排序。
        siftDown(0, x);
    setIndex(f, -1);
    return f;
}

这个方法与我们在第一节中,介绍堆的删除方法一样。

  1. 先将队列中元素个数减一。

  2. 将原队列末尾元素设置成队列头元素,再将队列末尾元素设置为null。

  3. 调用siftDown(0, x)方法,保证按照元素的优先级排序。

等待获取队列头元素

public RunnableScheduledFuture<?> take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            RunnableScheduledFuture<?> first = queue[0];
            // 如果没有任务,就让线程在available条件下等待。
            if (first == null)
                available.await();
            else {
                // 获取任务的剩余延时时间
                long delay = first.getDelay(NANOSECONDS);
                // 如果延时时间到了,就返回这个任务,用来执行。
                if (delay <= 0)
                    return finishPoll(first);
                // 将first设置为null,当线程等待时,不持有first的引用
                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 && queue[0] != null)
            // 唤醒等待任务的线程
            available.signal();
        lock.unlock();
    }
}

如果队列中没有任务,那么就让当前线程在available条件下等待。如果队列头任务的剩余延时时间delay大于0,那么就让当前线程在available条件下等待delay时间。

如果队列插入了新的队列头,它的剩余延时时间肯定小于原来队列头的时间,这个时候就要唤醒等待线程,看看它是否能获取任务。

超时等待获取队列头元素

public RunnableScheduledFuture<?> poll(long timeout, TimeUnit unit)
    throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            RunnableScheduledFuture<?> first = queue[0];
            // 如果没有任务。
            if (first == null) {
                // 超时时间已到,那么就直接返回null
                if (nanos <= 0)
                    return null;
                else
                    // 否则就让线程在available条件下等待nanos时间
                    nanos = available.awaitNanos(nanos);
            } else {
                // 获取任务的剩余延时时间
                long delay = first.getDelay(NANOSECONDS);
                // 如果延时时间到了,就返回这个任务,用来执行。
                if (delay <= 0)
                    return finishPoll(first);
                // 如果超时时间已到,那么就直接返回null
                if (nanos <= 0)
                    return null;
                // 将first设置为null,当线程等待时,不持有first的引用
                first = null; // don't retain ref while waiting
                // 如果超时时间小于任务的剩余延时时间,那么就有可能获取不到任务。
                // 在这里让线程等待超时时间nanos
                if (nanos < delay || leader != null)
                    nanos = available.awaitNanos(nanos);
                else {
                    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 && queue[0] != null)
            // 唤醒等待任务的线程
            available.signal();
        lock.unlock();
    }
}

与take方法相比较,就要考虑设置的超时时间,如果超时时间到了,还没有获取到有用任务,那么就返回null。其他的与take方法中逻辑一样。

文章作者: 刘同学
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 刘同学的小站
后端 Java
喜欢就支持一下吧