在最近的工做中,须要实现一个当一个任务执行完后,再等 100 毫秒而后再次执行的功能。当时最早反映到的就是 java 线程池的 ScheduledExecutorService,而 ScheduledExecutorService 有两个周期性执行任务的方法,分别是 scheduleAtFixedRate 与 scheduleWithFixedDelay,当时对这两个方法也不大了解,感受和个人理解有所误差,因此对这两个方法进行了研究。java
想要了解 scheduleWithFixedDelay 和 scheduleAtFixedRate 这两个周期性执行任务的方法,首先要了解 ScheduledExecutorService 的原理。在《java 并发编程的艺术》一书中有详细的解说,这里就简单的阐述一下。
ScheduledExecutorService 与其余线程池的区别,主要在于在执行前将任务封装为ScheduledFutureTask与其使用的阻塞队列DelayedWorkQueue。编程
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 是一个优先队列,在元素出队时,ScheduledFutureTask 的 time 最小的元素将优先出队,若是 time 值相同则判断 sequenceNumber,先入队的元素先出队。
而 DelayedWorkQueue 也是 ScheduledExecutorService 可以定时执行任务的核心类。
首先回顾一下线程池的执行流程:并发
当工做线程执行 DelayedWorkQueue 的出队方法时,DelayedWorkQueue 首先获取到 time 值最小的 ScheduledFutureTask,即将要最早执行的任务。而后用 time 值(任务要执行的时间戳)与当前时间做比较,判断任务执行时间是否到期,若然到期,元素立马出队,交由工做线程执行。
可是当 time 值还没到期呢?那么 time 将会减去当前时间,获得 delay 值(延迟多少时间后执行任务),而后使用方法Condition.awaitNanos(long nanosTimeout),阻塞获取任务的工做线程,直到通过了 delay 时间,即到达了任务的执行时间,元素才会出队,交由工做线程执行。ide
根据我以前的理解,认为 scheduleAtFixedRate 是绝对周期性执行,例如间隔周期为 10 秒,那么任务每隔 10 秒都会执行一次,无论任务是否成功执行。可是个人理解是错误的,这两个方法的功能分别是:线程
要清楚,一个定时任务,无论是否为周期性执行,都将会只由一条工做线程执行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 并不在行。