详解ScheduledExecutorService的周期性执行方法

详解 ScheduledExecutorService 的周期性执行方法

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

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 时间,即到达了任务的执行时间,元素才会出队,交由工做线程执行。ide

scheduleAtFixedRate 与 scheduleWithFixedDelay

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

  1. scheduleAtFixedRate:任务执行完成后,在提交任务到任务执行完成后的时间是否通过了 period,若然通过了,即立刻再次执行该任务。不然等待,直到提交任务到如今已经通过了 period 时间,再次执行该任务。
  2. scheduleWithFixedDelay:任务执行完成后,等待 delay 时间,而后再次执行。

要清楚,一个定时任务,无论是否为周期性执行,都将会只由一条工做线程执行code

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

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 对象,从新添加到队列中,等待下一次执行。
要清楚,不论调用哪一个周期性执行方法,都是须要等到任务逻辑执行完成后,才能再次添加到队列中,等待下一次执行。it

scheduleAtFixedRate 方法,每次都是在 time 的基础上添加 period 时间,若是任务逻辑的执行时间大于 period,那么在定时任务再次出队前,time 一定是小于当前时间,立刻出队被工做线程执行。由于 time 每次都是任务开始执行的时间点。
scheduleWithFixedDelay 方法,每次都将 time 设置为当前时间加上 period,那么轮到定时任务再次出队时,一定是通过了 period 时间,才能被工做线程执行。

总结

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

相关文章
相关标签/搜索