Java & Executor & ScheduledFutureTask & 总结

前言


 相关系列

 涉及内容

概述


 简介

在这里插入图片描述

    ScheduledFutureTask @ 调度未来任务类是代理任务功能/任务/延迟/周期性的实现封装。作为FutureTask @ 未来任务类/RunnableScheduledFuture @ 可运行调度未来接口的子类/实现类,调度未来任务类不但继承了未来任务类对代理任务功能/任务性的实现封装,还实现了可运行调度未来接口对代理任务延迟/周期性的概念封装,因此调度未来任务类可被用于开启/追踪/干预/获取/延迟/支持代理任务执行流程/状态/过程/结果/时间/周期。
 
 

使用


 创建

  • ScheduledFutureTask(Runnable r, V result, long ns) —— 创建指定可运行任务/结果/纳秒时间的单次调度未来任务。

  • ScheduledFutureTask(Runnable r, V result, long ns, long period) —— 创建指定可运行任务/结果/纳秒时间/周期的调度未来任务。如果周期 > 0意味着代理任务按指定速率执行;如果周期 = 0意味着代理任务单词执行;而如果周期 < 0则意味着代理任务按指定延迟执行。

  • ScheduledFutureTask(Callable callable, long ns) —— 创建指定可调用任务/纳秒时间的单次调度未来任务。
     

 方法

  • public boolean cancel(boolean mayInterruptIfRunning) —— 取消 —— 取消当前调度未来任务的代理任务,代理任务未结束(等待中/执行中)则返回true;否则返回false。当代理任务未结束时如果参数为false则方法将阻止等待中的代理任务执行,但不会中断执行中的代理任务(的执行线程);而如果参数为true则方法不但会阻止等待中的代理任务执行,还会中断执行中的代理任务(的执行线程)。当代理任务被成功取消后,该方法还会根据取消移除策略决定是否将当前调度未来任务从当前调度线程池执行器的延迟工作队列中移除。
        由于Java响应式中断的原因,执行线程的中断并不意味着任务执行的必然中断。因为这首先要求任务本身必须具备线程中断的响应能力/代码,其次还要保证线程必须中断在响应代码的执行上游…而显然这是无法100%实现的,因此代理任务并不会因为执行线程的的中断而必然停止执行…但这也并不影响方法返回true。即方法只在意取消是否执行,而不在意具体的取消结果。

  • public boolean isCancelled() —— 是否取消 —— 判断当前调度未来任务的代理任务是否取消,是则返回true;否则返回false。

  • public boolean isDone() —— 是否结束 —— 判断当前调度未来任务的代理任务是否结束(完成/异常/取消),是则返回true;否则返回false。*

  • public V get() —— 获取 —— 获取当前调度未来任务代理任务的执行结果。方法会无限等待至代理任务执行完成并返回结果;而如果代理任务执行异常则方法会抛出由运行异常(即任务执行抛出的异常)封装而来的执行异常;如果代理任务已被取消则方法会抛出取消异常;如果代理任务的执行线程被非cancel(boolean mayInterruptIfRunning)方法的方式中断则方法会抛出中断异常。此外,由于周期性代理任务永远无法执行完成,因此如果其未执行异常/取消/非法中断,则方法理论上会无限等待。

  • public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException —— 获取当前调度未来任务代理任务的执行结果。方法会有限等待至代理任务执行完成并返回结果;超出指定等待时间则抛出超时异常;如果代理任务执行异常则方法会抛出由运行异常封装而来的执行异常;如果代理任务已被取消则方法会抛出取消异常;如果代理任务的执行线程被非cancel(boolean mayInterruptIfRunning)方法的方式中断则方法会抛出中断异常。此外,由于周期性代理任务永远无法执行完成,因此如果其未执行异常/取消/非法中断,则方法理论上会抛出超时异常。

  • public void run() —— 运行 —— 执行当前调度未来任务的代理任务,并在周期执行的情况下设置下个执行时间/再次加入当前调度线程池执行器(的延迟工作队列)。
        run()方法是代理任务的执行/代理入口。调度未来任务类会在run()方法实现中执行代理任务,即调用内部可调用任务的call()方法,并在其前后执行内嵌的代理代码实现预设的各项代理功能…该方法的大致结构如下图所示:

在这里插入图片描述

  • public long getDelay(TimeUnit unit) —— 获取延迟 —— 获取当前调度未来任务代理任务指定时间单位的剩余延迟时间。

  • public int compareTo(Delayed other) —— 比较 —— 对比当前调度未来任务代理任务与指定延迟的剩余延迟时间,小于则返回-1;相同则返回0,大于则返回1。如果当前调度未来任务即为指定延迟,直接返回0;如果指定延迟也为调度未来任务,则在两者剩余延迟时间相同的情况下继续对比序号,小于则返回-1;否则返回1。序号由全局唯一的序号器生成,因此序号对比不会出现重复而返回0的情况。

  • public boolean isPeriodic() —— 是否周期 —— 判断当前调度未来任务的代理任务是否会周期执行,是则返回true;否则返回false。
     
     

实现


    调度未来任务类是未来任务类的子类,直接继承了其追踪/干预/获取代理任务执行状态/过程/结果的能力。因此本文只会重点讲解调度未来任务类的新增特性而不会对旧有内容进行详述,对该部分内容有需求的同学可通过本文头部的相关链接跳转至未来任务类的专项文章。
 

 调度(延迟/周期)执行

    调度未来任务类为代理任务的延迟执行提供了时间依据。调度未来任务类并不具备令代理任务直接延迟执行的能力,即其run()方法一旦被调度线程池执行器调用就意味着代理任务及代理行为会被立即执行。但这并不意味着调度未来任务类在代理任务的延迟执行中就毫无作用,因为如果没有其内部记录的时间数据作为延迟依据,调度线程池执行器类也是无法实现延迟执行的。

    调度线程池执行器只能从DelayedWorkQueue @ 延迟工作队列中获取到已延迟到期的可运行调度未来。所谓延迟到期是指可运行调度未来的剩余延迟时间,即Delayed.getDelay(TimeUnit unit)方法的返回值 <= 0。而所谓延迟工作队列类则是指调度线程池执行器类的静态内部类,被专用于存储/排序其所要调度执行的任务。该知识点会延迟工作队列类/调度线程池执行器类的专项文章中详述,此处我们只需记住以下三点即可:

  • 延迟工作队列类基于数组实现
  • 延迟工作队列类只支持保存可运行调度未来
  • 延迟工作队列类会将可运行调度未来根据剩余延迟时间进行顺序排序,这种排序的时间复杂度为O(log n)

    调度线程池执行器会试图从延迟工作队列中获取首个可运行调度未来以执行,因为在顺序排序下首个可运行调度未来的剩余延迟时间必然是最小的。因此如果首个可运行调度未来都未曾延迟到期,那就意味着整个延迟工作队列中都没有已延迟到期的可运行调度未来。执行线程将因此进入特定的等待状态,直至首个可运行调度未来延迟到期后将之移除/执行。该运行机制确保了调度线程池执行器能够获取/执行的必然已完成延迟的可运行调度未来。

    通过[sequenceNumber @ 序号]可以区分/排序剩余延迟时间相同的调度未来任务。当任务被递交至调度线程池执行器时,如果没有为之“装饰”指定的可运行调度未来接口实现类,则其会被默认封装为调度未来任务,并在[time @ 时间]中保存通过“递交时间 + 指定延迟时间”计算得到“纳秒”执行时间,以及生成一个全局唯一的long类型[序号]。因此在调度未来任务类中,判断延迟是否到期的本质即为判断[时间]是否 >= 当前时间。此外在“调度未来任务”之间的排序比较中,[时间]会取代剩余延迟时间成为直接比较依据,并在[时间]相同的情况下继续比较[序号]以进行排序,因此调度未来任务间的先后顺序是可以绝对保证的。

private static final AtomicLong sequencer = new AtomicLong();

ScheduledFutureTask(Runnable r, V result, long ns) {
    
    
    ... 
    // ---- 分配序号,sequencer是调度未来任务类中的全局静态变量。
    this.sequenceNumber = sequencer.getAndIncrement();
}

    调度未来任务类通过[period @ 周期]记录代理任务是否周期执行/周期执行方式/周期延迟时间。调度线程池执行器类有“速率/延迟”两类周期执行方式,其分别会令任务以上次执行的开始/结束时间为基数向后延迟指定时间后再执行。这其中相对特殊的是“速率”周期执行方式,因为其可能会出现任务执行时间长于周期延迟时间的情况。对此调度线程池执行器会将任务的后续执行延后,因此并不会出现并发执行的情况。调度未来任务类会将周期执行的相关数据统一保存在[周期]中,根据代理任务选择的调度方法不同,其所在调度未来任务的[周期]也会被赋予代表不同含义的值。[周期]为0表示代理任务只会被单次执行;而如果[周期] >/< 0则意味着代理任务会被“速率/延迟”周期执行,并且[周期]的绝对值会被作为周期延迟时间。

在这里插入图片描述

    调度未来任务的[时间]会合法溢出为负。long类型的[时间]字段虽然可以记录相当久远的执行时间,但“过于庞大延迟时间”与“周期执行的不断延迟”终究也会无法避免的导致[时间]溢出为负。不过调度未来任务类并没有阻止这种情况的发生,因为[时间]记录执行时间的核心目的是为了与当前时间/其它[时间]进行先后比较,从而判断代理任务是否延迟到期/可被执行;以及将之在延迟工作队列中按剩余延迟时间顺序排序。因此调度未来任务类只需保证先后比较的绝对正确性即可,并无需保证[时间]记录的绝对正确性。此外允许溢出为负还可以支持任务的无限周期执行,这同样也是无法拒绝的优势,因为[时间]能够准确记录的执行时间终究是有限的…Long类型数值的持续变化如下:

在这里插入图片描述

    与比较对象保持[0, Long.MAX_VALUE]的时间间隔可确保[时间]比较的绝对正确性。关于该范围内的[时间]为何能够正确比较此处不会详述,因为作者本人对此也未曾理解透彻,只知这是long类型数据表示/计算/存储机制而产生的必然结果。而为了保证[时间]能与当前时间正确比较以判断是否延迟到期,任务最多也只被允许延迟Long.MAX_VALUE纳秒。一个值得注意的点是:即使延迟时间被限制在[0, Long.MAX_VALUE]的范围内,任务也可能无法延迟指定时间。这是因为除延时到期外任务执行还受线程有无/执行时长等因素的影响,因此任务即使已经延迟到期也可能未被执行,而这就可能导致新任务在延迟工作队列中进行排序/比较时出现时间间隔超出Long.MAX_VALUE的情况…某具体实例如下:

在这里插入图片描述

    根据上图我们可知:调度未来任务在计算代理任务的[时间]时会与延迟工作队列中首任务进行判断,如果两者的时间间隔 > Long.MAX_VALUE,则为了保证排序/比较的正确性调度未来任务会适当的缩短延迟时间。而如果新代理任务的[时间]与首任务的时间间隔都介于[0, Long.MAX_VALUE]之间,那其与延迟工作队列后续任务的时间间隔就更不会出现超出范围的情况了…相关源码如下:

/**
 * Returns the trigger time of a delayed action.
 */
long triggerTime(long delay) {
    
    
    // ---- 判断新任务的执行时间与延迟工作队列的首任务的时间间隔是否可能会超出Long.MAX_VALUE。
    return now() + ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

/**
 * Constrains the values of all delays in the queue to be within Long.MAX_VALUE of each other, to avoid overflow in
 * compareTo. This may occur if a task is eligible to be dequeued, but has not yet been, while some other task is added
 * with a delay of Long.MAX_VALUE.
 */
private long overflowFree(long delay) {
    
    
    // ---- 基于延迟工作队列的首任务重新计算可延迟的最大时间。
    Delayed head = (Delayed) super.getQueue().peek();
    if (head != null) {
    
    
        long headDelay = head.getDelay(NANOSECONDS);
        if (headDelay < 0 && (delay - headDelay < 0))
            delay = Long.MAX_VALUE + headDelay;
    }
    return delay;
}

 执行

    调度未来任务类并没有对代理任务的执行制定特殊规则,因为其会直接调用父类未来任务的run()方法执行代理任务。但除此以外对于周期性代理任务调度未来任务还会执行部分附加操作,以确保代理任务在完成此次执行后会重新加入延迟工作队列以支持再次移除/执行…相关源码如下:

/**
 * Overrides FutureTask version so as to reset/requeue if periodic.
 */
public void run() {
    
    
    boolean periodic = isPeriodic();
    // ---- 如果当前调度线程池执行器的状态已已经不支持执行任务了,直接取消任务。
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // ---- 如果是非周期任务,直接调用父类FutureTask的run()方法执行任务。
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // ---- 如果是周期任务,直接调用父类FutureTask的runAndReset()方法执行任务,该方法与run()方法的区别在于
    // 不会设置<完成>状态,也不会保存任务执行结果。而任务执行成功后,方法还会周期任务的新一轮执行时间,并将任务
    // 重新加入延迟工作队列中以实现周期/重复执行。
    else if (ScheduledFutureTask.super.runAndReset()) {
    
    
        // ---- 计算/计算下个执行时间, 并将调度未来任务的[外部任务]重新加入延迟工作队列。正常情况下,调度未来
        // 任务与其[外部任务]时相同对象。
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

    调度未来任务类会使用/调用runAndReset()方法执行周期性代理任务。runAndReset()方法同样由未来任务类提供,其在逻辑上与run()方法高度一致,区别在于其不会在代理任务执行完成时保存结果,也不会将[state @ 状态]赋值为<COMPLETING @ 1 @ 完成中>/<NORMAL @ 2 正常>以禁止再度执行,即其并不存在“正常”链路。因此只要代理任务未被取消/抛出异常,那其就可以支持runAndReset()方法的不断调用以实现调度未来任务的周期调度…具体如下:

  • protected boolean runAndReset() —— 运行并重置 —— 不设置完成/不记录结果的执行当前调度未来任务的代理任务,并在可再次执行时返回true;否则返回false。

在这里插入图片描述

    调度未来任务会在周期性代理任务执行成功将[outerTask @ 外部任务]加入延迟工作队列。周期性代理任务被执行完成后,调度未来任务会根据“速率/延迟”周期执行方式基于[时间]/当前时间重新计算新一轮的[时间],这期间就可能会出现上文讲解过的“延迟时间会适当缩短”的情况。而在成功计算得到[时间]后调度未来任务则会将[外部任务]加入延迟工作队列中,以确保在下次延迟到期继续执行周期性代理任务。关于[外部任务]的存在可能会令人产生疑惑,而实际上该字段被用于保证自定义可运行调度未来接口实现类的重入队正确性。该知识点会在调度线程池执行器类的专项文章中详述,此处只需知道[外部任务]通常即为调度未来任务本身即可,因此将[外部任务]加入延迟工作队列实际上就是把当前调度未来任务加入了延迟工作队列。
 

 优化

    调度未来任务会在[heapIndex @ 堆索引]保存自身在延迟工作队列中的位置。由于延迟工作队列类是基于数组的实现,因此[堆索引]的存在可以帮助线程在常数时间内快速定位到指定调度未来任务在延迟工作队列中的位置,换句话说就是查找的时间复杂度为O(1)。这种优化虽然付出了一个额外字段的开销,但对查找性能的提升却非常明显,因为在正常情况下想要定位一个指定可运行调度未来就只能通过时间复杂度为O(n)的遍历操作实现。调度未来任务类之所以会设计该类优化是因为调度线程池执行器在运行过程中可能会因为周期性任务的取消而相对频繁地对可运行调度未来进行内部移除,而由于内部移除需要先对指定可运行调度未来进行定位,因此大量遍历必然会对调度线程池执行器的整体性能造成影响。而[堆索引]的存在避免了调度未来任务的遍历定位,从而就使得调度未来任务的内部移除流程可以与头部移除的流程相近,故而在时间复杂度上也由原本的O(n)下降为了O(log n)。O(log n)是可运行调度未来被内部移除后延迟工作队列在移除位置的基础上对剩余可运行调度未来进行下排序的时间复杂度,虽说与头部移除的时间复杂度相同,但实际由于下排序的起点位于延迟工作队列内部而非头部,因此实际时间消耗会比头部移除更少。

    延迟工作队列[堆索引]位置的可运行调度未来未必是指定调度未来任务。[堆索引]虽然可以快速定位位置,但这并不意味着找到的可运行调度未来与指定调度未来任务就是相同的。因为延迟工作队列类虽是调度线程池执行器类的内部类,并且实例也只会在其内部创建,但开发者依然可以通过执行器的getQueue()方法获取到延迟工作队列。因此从一个延迟工作队列中获取调度未来任务并将之从另一个延迟工作队列中移除的情况是可能存在的…故而如果发现并不相同则依然要进行遍历定位。此外,由于[堆索引]只能唯一保存,因此一个调度未来任务“在被移除前”不允许再次插入同一个延迟工作队列中。因为后一次插入会覆盖前一次插入维护的[堆索引],而这可能导致操作出现异常…以内部移除为例:内部移除的作用是移除指定可运行调度未来迭代器顺序的首个实例,而如果一个调度未来任务在一个延迟工作队列中被多次保存,那就可能出现移除非首个实例的情况。当然,如果是非调度未来任务类型的可运行调度未来那也没有这种要求。

    [堆索引]可以间接减少延迟工作队列中的垃圾遗留。所谓的垃圾是指无效可运行调度未来及其关联的相应对象,[堆索引]的存在并无法直接减少垃圾在延迟工作队列中的遗留,即其在内部运行流程方面并不会对GC产生额外收益。而之所以说[堆索引]可以间接减少延迟工作队列中的垃圾遗留是因为其大幅优化调度未来任务的定位后使得内部移除不再是一个低性能的操作,因此调度线程池执行器完全可以通过频繁调用延迟工作队列的remove(Object x)方法将无效调度未来任务内部移除来降低其中垃圾遗留的总量,而无需等待其被自然排序到延迟工作队列的顶部后再头部移除。

猜你喜欢

转载自blog.csdn.net/qq_39288456/article/details/143431735