图解DelayedWorkQueue核心方法

任务排序sift方法

private void siftUp(int k, RunnableScheduledFuture<?> key) {
// 找到父节点的索引
while (k > 0) {
  // 获取父节点
        int parent = (k - 1) >>> 1;
        RunnableScheduledFuture<?> e = queue[parent];
  // 如果key节点的执行时间大于父节点的执行时间,不需要再排序了
        if (key.compareTo(e) >= 0)
            break;
 // 如果key.compareTo(e) < 0,说明key节点的执行时间小于父节点的执行时间,需要把父节点移到后面
        queue[k] = e;
        setIndex(e, k);
// 设置索引为k
        k = parent;
    }
// key设置为排序后的位置中
    queue[k] = key;
    setIndex(key, k);
}

假设新入队的节点的延迟时间(调用getDelay()方法获得)是5,执行过程如下:

  1. 先将新的节点添加到数组的尾部,这时新节点的索引k为7:

  1. 计算新父节点的索引:parent = (k - 1) >>> 1,parent = 3,那么queue[3]的时间间隔值为8,因为 5 < 8 ,将执行queue[7] = queue[3]:

  1. 这时将k设置为3,继续循环,再次计算parent为1,queue[1]的时间间隔为3,因为 5 > 3 ,这时退出循环,最终k为3:

可见,每次新增节点时,只是根据父节点来判断,而不会影响兄弟节点。

siftDown方法

private void siftDown(int k, RunnableScheduledFuture<?> key) {
// 根据二叉树的特性,数组长度除以2,表示取有子节点的索引
    int half = size >>> 1;
// 判断索引为k的节点是否有子节点
    while (k < half) {
// 左子节点的索引
        int child = (k << 1) + 1;
        RunnableScheduledFuture<?> c = queue[child];
// 右子节点的索引
        int right = child + 1;
// 如果有右子节点并且左子节点的时间间隔大于右子节点,取时间间隔最小的节点
        if (right < size && c.compareTo(queue[right]) > 0)
            c = queue[child = right];
// 如果key的时间间隔小于等于c的时间间隔,跳出循环
        if (key.compareTo(c) <= 0)
            break;
// 设置要移除索引的节点为其子节点
        queue[k] = c;
        setIndex(c, k);
        k = child;
    }
// 将key放入索引为k的位置
    queue[k] = key;
    setIndex(key, k);
}

siftDown方法执行时包含两种情况,一种是没有子节点,一种是有子节点(根据half判断)。例如:

没有子节点的情况:

假设初始的堆如下:

假设 k = 3 ,那么 k = half ,没有子节点,在执行siftDown方法时直接把索引为3的节点设置为数组的最后一个节点:

有子节点的情况:

假设 k = 0 ,那么执行以下步骤:

1、获取左子节点,child = 1 ,获取右子节点, right = 2 :

2、由于 right < size ,这时比较左子节点和右子节点时间间隔的大小,这里 3 < 7 ,所以 c = queue[child] ;

3、比较key的时间间隔是否小于c的时间间隔,这里不满足,继续执行,把索引为k的节点设置为c,然后将k设置为child;

4、因为 half = 3 ,k = 1 ,继续执行循环,这时的索引变为:

5、这时再经过如上判断后,将k的值为3,最终的结果如下:

6、最后,如果在finishPoll方法中调用的话,会把索引为0的节点的索引设置为-1,表示已经删除了该节点,并且size也减了1,最后的结果如下:

可见,siftdown方法在执行完并不是有序的,但可以发现,子节点的下次执行时间一定比父节点的下次执行时间要大,由于每次都会取左子节点和右子节点中下次执行时间最小的节点,所以还是可以保证在take和poll时出队是有序的。

remove方法

public boolean remove(Object x) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int i = indexOf(x);
        if (i < 0)
            return false;

        setIndex(queue[i], -1);
        int s = --size;
        RunnableScheduledFuture<?> replacement = queue[s];
        queue[s] = null;
        if (s != i) {
  // 从i开始向下调整
            siftDown(i, replacement);
 // 如果queue[i] == replacement,说明i是叶子节点
            // 如果是这种情况,不能保证子节点的下次执行时间比父节点的大
            // 这时需要进行一次向上调整
            if (queue[i] == replacement)
                siftUp(i, replacement);
        }
        return true;
    } finally {
        lock.unlock();
    }
}

假设初始的堆结构如下:

这时要删除8的节点,那么这时 k = 1,key为最后一个节点:

这时通过上文对siftDown方法的分析,siftDown方法执行后的结果如下:

这时会发现,最后一个节点的值比父节点还要小,所以这里要执行一次siftUp方法来保证子节点的下次执行时间要比父节点的大,所以最终结果如下:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值