• ScheduledExecutorService中scheduleAtFixedRate方法与scheduleWithFixedDelay方法的区别


    ScheduledExecutorService中scheduleAtFixedRate方法与scheduleWithFixedDelay方法的区别

    1. ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,可以作为线程池来使用,同时实现了ScheduledExecutorService接口,来执行一些周期性的任务。ScheduledExecutorService一般常用的方法主要就4个

          public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
          public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
          public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
          public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay, long delay,TimeUnit unit);
      

      两个schedule方法都很明确,就是执行一次Runnable、Callable任务。

      scheduleAtFixedRate和scheduleWithFixedDelay这两个方法看起来就不是那么好区分了,今天就带大家从源码角度看看这两个方法的区别.

    2. 我们先看看这两个方法的区别

      下面是这两个方法的源码

    ​ 从上面的图上可以看到唯一的不同就是在创建ScheduledFutureTask对象的时候,scheduleWithFixedDelay将我们传入的delay取了个负数。所以这两个方法的区别都会在ScheduledFutureTask这个类中。

    ​ 先说下ScheduledFutureTask这个类吧,它是ScheduledThreadPoolExecutor的内部类,我们看下它的继承关系

    从上图中能看到ScheduledFutureTask,间接继承了Runnable接口,会实现run方法。而我们ScheduledThreadPoolExecutor类中真正执行任务的类其实也就是调用ScheduledFutureTask的run方法。也间接实现了Comparable接口的比较方法。

    1. 下面以scheduleAtFixedRate看看内部调用逻辑

          public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                        long initialDelay,
                                                        long period,
                                                        TimeUnit unit) {
              if (command == null || unit == null)
                  throw new NullPointerException();
              if (period <= 0L)
                  throw new IllegalArgumentException();
              ScheduledFutureTask<Void> sft =
                  new ScheduledFutureTask<Void>(command,
                                                null,
                                                //这里是计算首次执行实现
                                                triggerTime(initialDelay, unit),
                                                unit.toNanos(period),
                                                sequencer.getAndIncrement());
              //这里当前是直接返回的上面的sft,这个是留给子类去扩展的
              RunnableScheduledFuture<Void> t = decorateTask(command, sft);
              sft.outerTask = t;
              //所以这里也就是把上面创建的ScheduledFutureTask加到线程池的任务队列中去
              delayedExecute(t);
              return t;
          }
      

      这里是ScheduledFutureTask的构造方法

              ScheduledFutureTask(Runnable r, V result, long triggerTime,
                                  long period, long sequenceNumber) {
                  super(r, result);
                  //这个是任务首次执行的时间
                  this.time = triggerTime;
                  //这里的代码很简单,只是将它赋值给了成员变量period。
                  //其中scheduleWithFixedDelay是在外面取了个负数传了进来,scheduleAtFixedRate则是原样传了进来
                  this.period = period;
                  //这个是AtomicLong类型的,每次都+1,对我们加入的任务做了个编号
                  this.sequenceNumber = sequenceNumber;
              }
      

      我们再看看delayedExecute(t);的内部

          private void delayedExecute(RunnableScheduledFuture<?> task) {
              if (isShutdown())
                  reject(task);
              else {
                  //重点在这里,这里会把上面的ScheduledFutureTask加入到线程池的任务队列中,
                  //这里的super.getQueue()这个队列,是在ScheduledThreadPoolExecutor构造方法中定义的,也是ScheduledThreadPoolExecutord的内部类,类名是DelayedWorkQueue
                  //DelayedWorkQueue其实是一个最小堆,会对加入它的元素,调用compareTo方法进行排序,首个元素是最小的
                  //对应当前这里,就是调用ScheduledFutureTask的compareTo方法进行排序,也就是队列中的任务是按照执行时间的先后顺序排序的
                  //最终线程池执行任务的时候从首部依次获取task,具体获取任务的时候,DelayedWorkQueue会首先获取任务,查看对应的执行时间,如果任务时间没有到,就会调用Condition.awaitNanos去暂停,直到到达执行时间或者通过给队列中添加任务调用Condition.signal去唤醒
                  super.getQueue().add(task);
                  if (!canRunInCurrentRunState(task) && remove(task))
                      task.cancel(false);
                  else
                      //这里是根据线程池当前的线程数,如果小于核心线程数,就会新启动线程去执行任务
                      ensurePrestart();
              }
          }
      
      

      上面已经可以看到把任务已经加到线程池中去了,后面就是具体由线程池去执行任务了,所以我们直接去ScheduledFutureTask查看run方法就可以了

              public void run() {
                  if (!canRunInCurrentRunState(this))
                      cancel(false);
                  else if (!isPeriodic())
                      super.run();
                  //具体在这里会调用我们传入的run方法
                  else if (super.runAndReset()) {
                      //在这里会更新成员变量time,scheduleAtFixedRate和scheduleWithFixedDelay的区别也全在这里了,下面我们去看看这里
                      setNextRunTime();
                      //这里会重新将outerTask加入到线程池的任务队列中,这里的outerTask==我们当前执行run方法的对象this
                      reExecutePeriodic(outerTask);
                  }
              }
          }
      

      通过上面的代码也能看到,我们的run方法是不会同时由多次执行的,举个例子,如果我们调用scheduleAtFixedRate或者scheduleWithFixedDelay方法,传入的Runnable的对象,需要执行10s,而我们设定的周期是2s,是不会在第一次Runnable的10s的周期任务启动后2s,就启动第2次周期任务的。它只会在第一个Runnable的10s的周期任务结束后,重新加入到任务队列中之后,才会启动下次的任务。

          private void setNextRunTime() {
              //这里的period ,scheduleAtFixedRate传入的是正数,scheduleWithFixedDelay传入的是负数
              long p = period;
              if (p > 0)
                  //所以scheduleAtFixedRate会走这里,这里的time开始时时首次任务的开始执行时间,所以下次任务的时间就是(开始添加任务时计算出来的首次任务执行时间(这个时间不一定是任务首次执行的真正时间)+(任务执行次数-1)*period)
                  time += p;
              else
                  //这里对p取负,就会还原成正数,也就是我们最初调用scheduleWithFixedDelay时传入的值,这里的下次执行时间会用当前系统时间(可以看成当前Runnable执行的结束时间)+period来设置
                  time = triggerTime(-p);
          }
      
    2. 结论

      scheduleAtFixedRate或者scheduleWithFixedDelay对于从第2次开始的任务的计算时间不一样:

      • scheduleAtFixedRate 下次任务的时间=(开始添加任务时计算出来的首次任务执行时间+(任务执行次数-1)*period)
      • scheduleWithFixedDelay 下次任务的时间=当前任务结束时间+period

      需要注意的是,下次任务时间都只是计算出来的理论值,如果任务的执行时间大于周期任务的period,或者设置的线程池中线程太少,就会出现下次任务执行时间<时间任务执行时间

  • 相关阅读:
    php笔记--php安装
    IEDA-Git的应用
    php类型
    3.比较与逻辑运算
    2.阶乘
    1.双分支
    013_request对象包含的内容以及获取
    011_jsp引入JSTL后实现jsp的解耦
    010_jsp引入JSTL
    ORACLE1.28 面试题
  • 原文地址:https://www.cnblogs.com/wbo112/p/15203839.html
Copyright © 2020-2023  润新知