详解scheduleAtFixedRate与scheduleWithFixedDelay原理

详解scheduleAtFixedRate与scheduleWithFixedDelay原理前言前几天,肥佬分享了一篇关于定时器的文章你真的会使用定时器吗?,从使用角度为我们详细地说明了定时器的用法,包括fixedDelay、fixedRate,为什么会有这样的区别呢?下面我们从源码角度分析下二者的区别与底层原理。jdk定时器这里不再哆嗦延迟队列、线程池的知识了,请移步下面的链接延迟队列原理,http://cmsblogs.com/?p=2448线程池原理,http://…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

前言

前几天,肥佬分享了一篇关于定时器的文章你真的会使用定时器吗?,从使用角度为我们详细地说明了定时器的用法,包括 fixedDelay、fixedRate,为什么会有这样的区别呢?下面我们从源码角度分析下二者的区别与底层原理。

jdk 定时器

这里不再哆嗦延迟队列、线程池的知识了,请移步下面的链接

  • 延迟队列原理,http://cmsblogs.com/?p=2448
  • 线程池原理,http://cmsblogs.com/?p=2448

可能大家对 ScheduledThreadPoolExecutor 并不陌生,它便是我们常用的定时器,即便如此,仍然有很多小伙伴使用 API 的姿势不正确,更别说底层原理了。我非常负责任地告诉你,定时器的原理很简单,我们可以把它看成是延迟队列 + 线程池的加强版,我们都知道线程池需要从队列中获取任务,如果我们在指定的时间(定时调度)才能从队列中获取任务,那么这个调度任务便可以在指定时间被执行。那么如何才能在指定时间从队列中获取任务呢?这个得借助延迟队列(java.util.concurrent.ScheduledThreadPoolExecutor.DelayedWorkQueue),如果延迟队列达到临界条件那么这个任务便可以出队列了(临界条件:当前时间已经到达下次运行时间 nextRunTime ),然后由线程池中的线程获取到该任务并运行该任务。

下图描述了 ScheduledThreadPoolExecutor 的原理,线程从延迟队列中阻塞获取任务,直到该任务到达下一次运行时间,线程拿到该任务后调用任务的 run() 方法执行任务,运行完之后,设置下一次运行的时间,再扔到延迟队列中,这样便又可以在下一次调度时间拿到该任务,并调度该任务,从而构成一个闭环操作,完成任务的定时调度,这个便是调度线程池的核心原理了。

ScheduledThreadPoolExecutor原理图

我们熟悉的 scheduleAtFixedRate、scheduleWithFixedDelay 方法,还有 cron 表达式,他们的主要区别在于计算下一次调度时间的逻辑不同,这样导致调度的效果有很大的区别

我们先来看看类图:

scheduled类图

由类图可知,ScheduledThreadPoolExecutor 继承至线程池 ThreadPoolExecutor,并且它提供了 schedule、scheduleAtFixedRate、scheduleWithFixedDelay 方法的扩展

  • schedule(Runnable command, long delay, TimeUnit unit):在指定的延迟时间(delay)之后调度,并且只会调度一次
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):在指定的延迟时间( delay)调度第一次,后续以 period 为一个时间周期进行调度,该方法并不 care 每次任务执行的耗时,如果某次耗时超过调度周期(period),则下一次调度从上一次任务结束时开始
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):在指定的延迟时间( delay)调度第一次,后续以 period 为一个时间周期进行调度,该方法非常 care 上一次任务执行的耗时,如果某次耗时超过调度周期(period),则下一次调度时间为 上一次任务结束时间 + 调度周期时间

其实从字面意思,也可以猜测其运行效果,at 是指到达对应的时间点,而 with 是有夹带的意思

下面我们来分析一波源码

scheduleAtFixedRate & scheduleWithFixedDelay

scheduleAtFixedRate 方法的逻辑很简单,只是构造了一个 ScheduledFutureTask 任务,然后丢到延迟队列中,具体的代码如下所示:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
    // 省略参数校验代码......
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),  // (1)
                                      unit.toNanos(period));
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);       // (2)
    sft.outerTask = t;
    delayedExecute(t);                                                  // (3)
    return t;
}

Jetbrains全家桶1年46,售后保障稳定

  • (1). 构造 ScheduledFutureTask 对象,triggerTime 方法计算了第一次调度的时间,unit.toNanos(period) 也是将调度周期转换为纳秒,这个地方便是 scheduleAtFixedRate 和 scheduleWithFixedDelay 方法的主要区别,后者传入的是负数unit.toNanos(-period)
  • (2). 包装 ScheduledFutureTask,方便子类扩展
  • (3). 将任务丢到延迟队列中,并且创建线程,然后线程会从延迟队列中阻塞获取队列中的任务,然后再就是运行任务了,再然后请看下文的分析

ScheduledFutureTask 是一个内部类,它实现了 Runnable 接口,构造函数如下所示,我们重点关注下第三个、第四个参数 ns、period,ns 参数会赋值给成员变量 time 代表任务第一次调度的时间,而 period 代表调度周期,scheduleAtFixedRate 方法传入 period 的是正数,scheduleWithFixedDelay 传入的是负数。

ScheduledFutureTask(Runnable r, V result, long ns, long period) {
    super(r, result);
    this.time = ns;
    this.period = period;
    this.sequenceNumber = sequencer.getAndIncrement();
}

ScheduledFutureTask 被扔到延迟队列中,那什么时候可以出队列呢?它实现了 Delayed 接口,如果该值返回负数便可以出队列了(调度时间小于当前时间)。出队列后,然后由线程池中的 Thread 调用其 run() 方法.

public long getDelay(TimeUnit unit) {
    return unit.convert(time - now(), NANOSECONDS);
}

public void run() {
    boolean periodic = isPeriodic();        // (1)
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
        ScheduledFutureTask.super.run();    // (2)
    else if (ScheduledFutureTask.super.runAndReset()) {     // (3)
        setNextRunTime();                   // (4)
        reExecutePeriodic(outerTask);       // (5)
    }
}
  • (1). 判断是不是周期性调度,通过构造函数传入的 period 值判断,如果大于0,则说明是周期性调度,否则只调度一次
  • (2). 如果不是周期性调度,只调度一次
  • (3). 运行调度任务
  • (4). setNextRunTime 方法会计算下一次运行时间(重要)
  • (5). 将任务重新丢到队列中,如果有必要的话,会创建线程来执行任务

分析到这里,调度线程池的原理已经水落石出了,我们再来研究下 setNextRunTime。前面也说了,scheduleAtFixedRate、scheduleWithFixedDelay 这两个 api 方法传递的 period 值是有正负之分的,因此计算下一次调度时间也是有差异的,具体代码如下:

private void setNextRunTime() {
    long p = period;
    if (p > 0) 
        time += p;              // (1)
    else
        time = triggerTime(-p); // (2)
}
  • scheduleAtFixedRate 方法对应的调度周期 period 大于0,走逻辑(1),下一次调度时间 = 上一次调度时间 + 调度周期,试想如果任务执行的耗时大于调度周期 period,那么指定的下一次调度时间会小于当前时间,意味着这个任务又可以从延迟队列中移出,立马被执行
  • scheduleWithFixedDelay 方法对应的 period 小于0,走逻辑(2),变量 p 是负责,调用 triggerTime 方法得到的时间是 当前时间(当前任务结束时间) + 调度周期,由此可见,上一次任务的执行结束时间起到了关键作用,不管上一次任务耗时是否超过 period 周期,下一次任务的开始时间始终从上一次结束时间开始计算

写在最后

关于 spring-schedule 的代码,在此不再做过多的分析,底层实现仍然是 jdk 的定时器 ScheduledThreadPoolExecutor,而我们熟悉的 cron 表达式便是计算下一次调度时间的关键,感兴趣的同学可以查看以下相关代码

  • ScheduledAnnotationBeanPostProcessor,处理 Scheduled 注解,封装任务并交给定时器执行
  • org.springframework.scheduling.support.CronTrigger,根据 cron 表达式计算下一次调度时间
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/206672.html原文链接:https://javaforall.net

赞 (0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • webservice传参数_java有参构造方法

    webservice传参数_java有参构造方法一、获取接口信息:  使用工具soapUI获取接口调用信息:  双击request:复制接口调用格式:webService接口通常传递xml参数因此需要组装数据: ①若传递单个参数则:&lt;soapenv:Envelopexmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"xmlns:cen…

    2022年10月16日
  • MSM8953配置I2C及SPI

    MSM8953配置I2C及SPI此次完成的任务是要使能高通8953平台的i2c和spi,主要做的工作就是在设备树文件中添加节点信息。主要的工作在于对设备树文件的修改,主要修改了msm8953-pinctrl.dtsi和msm8953.dtsi两个文件。msm8953-pinctrl.dtsi是配置MSM8953芯片中的GPIO。在此文件中定义i2c使用哪个gpio。因为引脚复用功能的存在,所以要先配置i2c的引脚复用功能…

    2022年10月18日
  • 假装自己是黑客的代码_假装黑客的软件

    假装自己是黑客的代码_假装黑客的软件假装自己是“黑客”今天分享一个好玩的东西话不多说上图看起来是不是很酷炫?这个可以通过按数字或鼠标点击来打开字雨,卫星地图等页面。还有一些功能可以自己无聊的时候试一试哦。怎么做呢?一,最简单的方法在网页上输入网址:http://geekprank.com/hacker/二,如果要让别人看起来你厉害一点1.新建一个文档2.输入starthttp://geekprank.com/hacker/3.保存,名字自取,改后缀名为bat然后就能通过双击这个文件打开网址啦。。。三,实在懒得

    2022年9月28日
  • gg修改器如何使用修改迷你世界_gg游戏修改器迷你世界

    gg修改器如何使用修改迷你世界_gg游戏修改器迷你世界gg修改器脚本大全迷你世界设置回点发布时间:2020-08-20来源:脚本之家点击:->Imail的所有邮局信息,比如用户,密码都实际上都是存储在计算机注册表当中的,所以只需要打开注册表就可以看到Imail里的所有信息,包括用户的密码.细节:Imail将企业邮局信息全部存储在:HKEY_LOCAL_MACHINE\SOFTWARE\Ipswitch\IMail\Domains\\U…

    2022年9月5日
  • YUI Compressor

    YUI Compressor简介根据雅虎卓越性能团队的说法,40%到60%的雅虎用户拥有空闲缓存体验,所有页面浏览量中约有20%是使用空缓存完成的(请参阅TenniTheurer在YUIBlog上的这篇文章)有关浏览器缓存使

    2022年7月1日
  • java的编程思想是什么’_编程思想 是什么

    java的编程思想是什么’_编程思想 是什么未完待续,预计4.4完成1什么是项目在既定的资源和要求的约束下,为实现某种目的而相互联系的一次性工作任务。项目可以创造:1.一个产品;2.一种服务或提供服务的能力;3.对现有产品线或服务的改进;4.一种成果。项目的两大特性:1.临时性(Temporary)项目有明确的起点和终点,临时性并不意味着持续时间短,很多项目的结果具有持久性2.独特性(Unique)项目创造独特的可交互成果(Deliver…

    2022年7月8日

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号