详解ScheduledExecutorService的周期性执行方法

详解 ScheduledExecutorService 的周期性执行方法

在最近的工作中,需要实现一个当一个任务执行完后,再等 100 毫秒然后再次执行的功能。当时最先反映到的就是 java 线程池的 ScheduledExecutorService,而 ScheduledExecutorService 有两个周期性执行任务的方法,分别是 scheduleAtFixedRate 与 scheduleWithFixedDelay,当时对这两个方法也不大了解,感觉和我的理解有所偏差,所以对这两个方法进行了研究。

ScheduledExecutorService 的基本原理

想要了解 scheduleWithFixedDelay 和 scheduleAtFixedRate 这两个周期性执行任务的方法,首先要了解 ScheduledExecutorService 的原理。在《java 并发编程的艺术》一书中有详细的解说,这里就简单的阐述一下。
ScheduledExecutorService 与其他线程池的区别,主要在于在执行前将任务封装为ScheduledFutureTask与其使用的阻塞队列DelayedWorkQueue

ScheduledFutureTask

    private class ScheduledFutureTask<V>
            extends FutureTask<V> implements RunnableScheduledFuture<V> {

        /** 表示这个任务添加到ScheduledExecutorService中的序号 */
        private final long sequenceNumber;

        /** T表示这个任务将要被执行的具体时间(时间戳) */
        private long time;

        /**
         * 表示任务执行的间隔周期,若为0则表示不是周期性执行任务
         */
        private final long period;

        /*省略以下代码*/

    }

DelayedWorkQueue

DelayedWorkQueue 是一个优先队列,在元素出队时,ScheduledFutureTask 的 time 最小的元素将优先出队,如果 time 值相同则判断 sequenceNumber,先入队的元素先出队。
而 DelayedWorkQueue 也是 ScheduledExecutorService 能够定时执行任务的核心类。
首先回顾一下线程池的执行流程:

  1. 向线程池提交任务,这时任务将入队到该线程池的阻塞队列
  2. 工作线程不断从队列中取出任务,并执行,若然队列中没有任务,工作线程将阻塞直到任务的到来。

当工作线程执行 DelayedWorkQueue 的出队方法时,DelayedWorkQueue 首先获取到 time 值最小的 ScheduledFutureTask,即将要最先执行的任务。然后用 time 值(任务要执行的时间戳)与当前时间作比较,判断任务执行时间是否到期,若然到期,元素立马出队,交由工作线程执行。
但是当 time 值还没到期呢?那么 time 将会减去当前时间,得到 delay 值(延迟多少时间后执行任务),然后使用方法Condition.awaitNanos(long nanosTimeout),阻塞获取任务的工作线程,直到经过了 delay 时间,即到达了任务的执行时间,元素才会出队,交由工作线程执行。

scheduleAtFixedRate 与 scheduleWithFixedDelay

根据我之前的理解,认为 scheduleAtFixedRate 是绝对周期性执行,例如间隔周期为 10 秒,那么任务每隔 10 秒都会执行一次,不管任务是否成功执行。但是我的理解是错误的,这两个方法的功能分别是:

  1. scheduleAtFixedRate:任务执行完成后,在提交任务到任务执行完成后的时间是否经过了 period,若然经过了,即马上再次执行该任务。否则等待,直到提交任务到现在已经经过了 period 时间,再次执行该任务。
  2. scheduleWithFixedDelay:任务执行完成后,等待 delay 时间,然后再次执行。

要清楚,一个定时任务,不管是否为周期性执行,都将会只由一条工作线程执行

首先看下这两个方法的源码

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                long initialDelay,
                                                long period,
                                                TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                        null,
                                        triggerTime(initialDelay, unit),
                                        unit.toNanos(period));
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                    long initialDelay,
                                                    long delay,
                                                    TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                        null,
                                        triggerTime(initialDelay, unit),
                                        unit.toNanos(-delay));
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}

其实两个方法没有太大区别,只是在构建 ScheduledFutureTask 的时候,ScheduledFutureTask 的 period 属性有正负差别,scheduleAtFixedRate 方法构建 ScheduledFutureTask 的 period 为负数,而 scheduleWithFixedDelay 为正数。
接下来查看 ScheduledFutureTask 的 run 方法,工作线程在执行任务时将会调用该方法

/**
 * Overrides FutureTask version so as to reset/requeue if periodic.
 */
public void run() {
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);//1
    else if (!periodic)
        ScheduledFutureTask.super.run();//2
    else if (ScheduledFutureTask.super.runAndReset()) {
        //3
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

如果定时任务时周期性执行方法,将会进入到 3 的执行逻辑,当然在这之前将会调用 runAndReset 执行任务逻辑。
当任务逻辑执行完成后,将会调用 setNextRunTime。

/**
 * Sets the next time to run for a periodic task.
 */
private void setNextRunTime() {
    long p = period;
    if (p > 0)
        time += p;
    else
        time = triggerTime(-p);
}

long triggerTime(long delay) {
    return now() +
        ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

如果 period 为正数数,即执行的方法为 scheduleAtFixedRate,在任务的执行时间上添加 period 时间。
而 period 为负数,即执行的方法为 scheduleWithFixedDelay,将 time 改写为当前时间加上 period 时间。
执行完 setNextRunTime 方法后,将执行 reExecutePeriodic 方法,即重新将该 ScheduledFutureTask 对象,重新添加到队列中,等待下一次执行。
要清楚,不论调用哪个周期性执行方法,都是需要等到任务逻辑执行完成后,才能再次添加到队列中,等待下一次执行。

scheduleAtFixedRate 方法,每次都是在 time 的基础上添加 period 时间,如果任务逻辑的执行时间大于 period,那么在定时任务再次出队前,time 必定是小于当前时间,马上出队被工作线程执行。因为 time 每次都是任务开始执行的时间点。
scheduleWithFixedDelay 方法,每次都将 time 设置为当前时间加上 period,那么轮到定时任务再次出队时,必定是经过了 period 时间,才能被工作线程执行。

总结

对于 ScheduledExecutorService 一定要清楚,周期性执行任务,一定是等到上一次执行完成后,才能再次执行,即每个任务只由一条线程执行。那么要实现当达到一定时候后,不论任务是否执行完成,都将再次执行任务的功能,ScheduledExecutorService 的两个周期性执行方法都是不能实现的。其实也就是对于复杂的时间调度控制,ScheduledExecutorService 并不在行。

转载于:https://my.oschina.net/bingzhong/blog/1559849

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值