ScheduledThreadPoolExecutor之scheduleWithFixedDelay和scheduleAtFixedRate的区别

结论

其实自己在看这个源码的时候,只是看到这两个方法都是周期性执行任务的,但是没有仔细去看两个方法的细节,所以,这篇笔记主要记录两者的区别
整个源码细节看下来之后,我认为这两个方法最大的一个区别是:
scheduleAtFixedRate是在上一次任务的开始时间的基础之上,加上指定的时间间隔,作为当前任务执行的开始时间
scheduleWithFixedDelay是在上一次任务执行完毕之后的基础之上,加上指定的时间间隔,作为当前任务执行的开始时间

在这里插入图片描述
我们假如定时执行周期都设置为5S,任务执行需要耗时2S,以这个窗口作为执行时间刻度的话,区别就是上面图中所画的这样

应用

这个代码的意思是:初始化了一个ScheduledThreadPoolExecutor,然后执行执行间隔是5S,每个任务中休眠了2S,模拟需要耗时2S的操作

scheduleAtFixedRate

public class ScheduledThreadPoolTest {
    
    
    public static void main(String[] args) {
    
    
        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(4);
        SimpleDateFormat sm = new SimpleDateFormat("hh:MM:ss");
        System.out.println("当前时间是:"+sm.format(new Date()));
        scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("这是执行的内容:" + sm.format(new Date()));
            }
        }, 5, 5, TimeUnit.SECONDS);
    }
}

打印结果:

当前时间是:08:03:57
这是执行的内容:08:03:04
这是执行的内容:08:03:09
这是执行的内容:08:03:14
这是执行的内容:08:03:19

scheduleWithFixedDelay

public class ScheduledThreadPoolTest {
    
    
    public static void main(String[] args) {
    
    
        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(4);
        SimpleDateFormat sm = new SimpleDateFormat("hh:MM:ss");
        System.out.println("当前时间是:"+sm.format(new Date()));
        scheduledThreadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("这是执行的内容:" + sm.format(new Date()));
            }
        }, 5, 5, TimeUnit.SECONDS);
    }
}

执行结果:

当前时间是:08:03:06
这是执行的内容:08:03:13
这是执行的内容:08:03:20
这是执行的内容:08:03:27
这是执行的内容:08:03:34

根据以上两个结果可以看到,在我们指定了执行周期都是5S的情况下,scheduleAtFixedRate是以第一次任务开启的时间作为起始值 + 5S,不管这个任务执行耗时是多久,都是5S开启一次
scheduleWithFixedDelay是以第一次任务结束的时间作为起始值 + 5S,作为下一次任务开始的时间

源码

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;
    }

对于上面两个方法的源码,其实我们可以发现,逻辑基本是一样的,只是有一个区别:
scheduleWithFixedDelay在new ScheduledFutureTask的时候,将delay参数设置为了负数,这个区别点,就是为什么fixedDelay是以上一次任务结束时间 + 周期间隔作为下次任务开始时间的原因

1.首先是判断任务或者unit是否为null,如果true,就抛出空指针异常
2.如果执行间隔周期小于0,也抛出异常
3.然后根据当前任务和执行间隔周期值生成对应的scheduledFutureTask对象;这里其实就是对要执行的任务进行了一层封装,添加了一些额外的参数
4.将包装好的任务保存在一个全局变量中
5.delayedExecute()是将任务添加到线程池的队列中,需要注意的是:这里会添加到scheduledThreadPoolExecutor的内部队列 delayWorkQueue中

对于这么一个方法,要做的事情就完成了,那我们需要考虑下,是如何做到定时执行的?其实原理很简单,在执行任务的时候,如果需要重复执行,就再次把任务入队即可
入队之后优先级的判断,是delayWordQueue自己内部的逻辑,
那我入队了一个任务之后,是谁去取出这个任务执行的呢?
这个问题我之前一直在迷茫,结果后来才想到,scheduledThreadPoolExecutor本身就是一个线程池啊,那当然是threadPoolExecutor中的逻辑去依次取出队列中的任务去执行,只是在调用出队方法的时候,会调用delayWordQueue的出队方法
这样看起来,底层源码的设计就清晰明了的了,各个组件或者各个模块负责自己的事情,如果你要对我所负责的事情进行扩展,没关系,只要按照我的要求来就可以了

对于delayWorKQueue出队、入队之后优先级的处理就不说了,我们接着来说如何重复执行

前面有说过,对于程序员在调用了scheduleAtFixedRate()方法之后,会把要执行的任务包装成ScheduledFutureTask对象,所以,线程池在执行任务的时候,调用的也是ScheduledFutureTask的run()方法

/**
 * Overrides FutureTask version so as to reset/requeue if periodic.
 * 自定义的任务在执行的时候,实际调用的就是这个方法,因为线程池对任务进行了一层包装
 */
public void run() {
    
    
    /**
     * 1.首先判断是否需要重复执行,这个值是在初始化的时候,指定的
     * 如果只需要执行一次,这里返回的就是false
     * 如果需要周期定时执行,这里返回的就是true
     * 根据period的值来判断
     */
    boolean periodic = isPeriodic();
    /**
     * 2.这里没看懂,判断是否需要取消任务?
     */
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    /**
     * 3.如果只需要执行一次,就会执行这里的逻辑
     */
    else if (!periodic)
        ScheduledFutureTask.super.run();
    /**
     * 4.如果是需要重复执行的,就执行这里的方法
     * 如果任务正常执行成功,就继续设置下次的执行时间
     * setNextRunTime():是在当前时间的基础之上,加上第一次指定的延迟时间
     * reExecutePeriodic():是将任务再次加入队列中
     */
    else if (ScheduledFutureTask.super.runAndReset()) {
    
    
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

isPeriodic()方法就是判断period是否为0
return period != 0;

那period是在什么时候赋值的呢?在new ScheduledFutureTask()的时候,这里就和上面我说 scheduleWithFixedDelay()方法,把周期执行间隔设置为负数对应上了
也就是说如果是周期执行的方法,period都不为0

runAndReset()方法是去调用程序员指定的要执行的代码块,也就是所谓的任务,执行成功之后,会接着调用两个方法

setNextRunTime()方法是设置下次任务执行时间
reExecutePeriodic()其实就是把任务再次入队

/**
 * Sets the next time to run for a periodic task.
 * 设置下次运行时间,
 * 如果是fixedRate,就是在当前time的基础上 + 初始化时指定的时间间隔
 * 如果是fixedDelay,就是在当前时间now()的基础上 + 指定的时间间隔
 */
private void setNextRunTime() {
    
    
    long p = period;
    if (p > 0)
        time += p;
    else
        time = triggerTime(-p);
}

首先需要知道:
scheduleAtFixedRate方法的period是大于0的
scheduleWithFixedDelay的period是小于0的

这里可以看到,对于time(任务开始执行时间)的设置,对于p > 0的场景,是在上次任务开始时间的基础上,加上设置的执行间隔
对于p < 0的场景,是调用了triggerTime(-p);方法


/**
 * Returns the trigger time of a delayed action.
 * 在fixedDelay方法被调用的时候,如果是设置下一次执行时间,就是now() + delay
 */
long triggerTime(long delay) {
    
    
    return now() +
        ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

这个判断中,就是now() + delay 因为delay一般设置的都是小于 Long.MAX_VALUE >> 1的

所以我们也可以推断出:
scheduleAtFixedRate方法是在上一次任务执行的开始时间的基础上 + 任务周期执行间隔作为下次任务开始时间
scheduleWithFixedDelay方法是在上一次任务执行完毕的时间基础上 + 任务周期执行间隔作为下次任务开始时间

猜你喜欢

转载自blog.csdn.net/CPLASF_/article/details/114651223