结论
其实自己在看这个源码的时候,只是看到这两个方法都是周期性执行任务的,但是没有仔细去看两个方法的细节,所以,这篇笔记主要记录两者的区别
整个源码细节看下来之后,我认为这两个方法最大的一个区别是:
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方法是在上一次任务执行完毕的时间基础上 + 任务周期执行间隔作为下次任务开始时间