Java线程池的基本实现ThreadPoolExecutor是如何实现的,主要是通过将Runnable或Callable实现类,包装成FutureTask,然后在维护的workers线程池集合中某一线程中运行run()方法,启动线程。这里只是大致说下原理,具体可参考上篇文章中的分析。我们本篇是对线程池的续集,解说下带有定时功能的ScheduledThreadPoolExecutor是如何实现的。本篇我们换一种方式开始这个话题,分别从问题提出,问题分析,问题总结三方面进行分析,这或许将是本人以后的写作风格。
提出问题
在提出问题之前,我们先看下ScheduledThreadPoolExecutor是什么,怎么运行定时运行一个定时任务。以下一段代码是ScheduledThreadPoolExecutor的基本使用方法
public class ScheduledThreadPoolExecutorTest {
public static void main(String[] args) {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(3);
// 延迟调度
threadPoolExecutor.schedule(new RunnableTest("test1"), 1, TimeUnit.SECONDS);
// 延迟后,周期性调度
threadPoolExecutor.scheduleAtFixedRate(new RunnableTest("test2"), 0, 1, TimeUnit.SECONDS);
// 延迟后,周期性调度
threadPoolExecutor.scheduleWithFixedDelay(new RunnableTest("test3"), 0, 1, TimeUnit.SECONDS);
}
public static class RunnableTest implements Runnable {
private String name;
RunnableTest(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + " time: " + new Date());
try {
// 特别注意此处睡眠时间
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果如下:
我们先来看下这段代码,main方法中注意是构造了一个核心线程数为3的ScheduledThreadPoolExecutor对象,然后调用提供的基本的三个调度方法。我们先根据这个结果分析下。
- test1任务,在test2/test3后延迟1秒执行,之后后,没有再调度
- test2任务,没有延迟执行,并且每次调用,间隔2s时间
- test3任务,没有延迟执行,并且每次调用,间隔3s时间
上面结果,除了test1外,test2/test3两个任务,看起来都挺奇怪,明明设置的是间隔1s时间调度,为什么出现的结果是间隔3s。这里我们注意到RunnableTest的run方法中,执行了Thread.sleep(2000),睡眠了2s时间,这里我不进一步验证test2/test3为什么出现这样的情况,而直接给出结论,若想验证以下结论,可以通过修改Thread.sleep睡眠时间验证。
- 从test1任务看出,执行schedule(Runnable command, long delay, TimeUnit unit)方法,可以延迟调度,不能周期性调度
- 从test2任务看出,执行scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)方法,可以延迟调度,并且可以周期性调度,不过周期时间有两种:若当前任务在下一个周期时间未到之前完成,则按照周期间隔时间执行;若当前任务在下一个周期时间到达后未完成,则按照任务执行完成时间开始执行调度。即下一次任务调度完成时间,取下一次调度时间,和任务执行时长中,取较大值。
- 从test3任务看出,执行scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)方法,可以延迟调度,并且可以周期性调度,下次任务执行时间为,当前任务执行完成后,延迟period时间开始执行。
以上内容为ScheduledThreadPoolExecutor的基本用法,各类文章中也都有介绍,那么我们可以带着一下问题,看下ScheduledThreadPoolExecutor如何实现定时调度。
- 如何实现一次性延迟调度
- 如何实现周期性固定频率调度
- 如何实现周期性固定延迟调度
问题分析
我们先看下ScheduledThreadPoolExecutor的集成依赖关系
从继承关系图,我们可以看出ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,所以他具有了ThreadPoolExecutor中的所有特征,已经调度方式,可以参考上一篇 Java 线程池之 ThreadPoolExecutor 源码分析 来了解ThreadPoolExecutor的实现,本文重点还是ScheduledThreadPoolExecutor。
我们首先从main方法中的ScheduledThreadPoolExecutor实例化开始,一步一步跟踪看下如何实现,首先看构造方法。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}
这里可以看出,其实就是调用了父类的构造方法,主要需要注意的是DelayedWorkQueue这个队列,这里是实现延迟调度的关键之处,后续将会分析DelayedWorkQueue的实现。
我们继续从main方法中的schedule(Runnable command, long delay, TimeUnit unit)看
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
// decorateTask默认实现只是单纯的返回第二个参数task,可以通过继承的方式扩展
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
这里decorateTask()方法只是返回了第二个参数,我们看下ScheduledFutureTask具体是什么
这里可以看出ScheduledFutureTask是FutureTask的一个具体实现,在 Java 线程池之 ThreadPoolExecutor 源码分析 中,已经对FutureTask中做了详细的讲解,这里不多赘述。根据上面代码,我们可以知道ScheduledThreadPoolExecutor是将Runnable实例,包装成了ScheduledFutureTask,通过以下构造方法。
ScheduledFutureTask(Runnable r, V result, long ns) {
super(r, result);
this.time = ns;
this.period = 0;
this.sequenceNumber = sequencer.getAndIncrement();
}
注意,这里的this.period设置了默认值0,表示没有周期调度。至于ns(任务运行时间),则通过triggerTime(delay, unit)方法开始执行获取,看下具体实现:
private long triggerTime(long delay, TimeUnit unit) {
return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}
long triggerTime(long delay) {
return now() + ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
我们继续回到schedule方法中,继续往下看,执行了delayedExecute(t);方法,具体实现如下:
private void delayedExecute(RunnableScheduledFuture<?> task) {
// 如果shutdown,则拒绝任务
if (isShutdown())
reject(task);
else {
// 添加到等待队列中
super.getQueue().add(task);
// 重新判断是否shutdown,并且当前状态不能运行当前任务,则尝试将任务移除,并且取消任务
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
// 否则开始确保任务准备开始
else
ensurePrestart();
}
}
代码注释中,已经解释了各个代码的作用,当任务可以运行时,将执行ensurePrestart()方法,
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
这里判断了当前线程数,是否达到核心线程数,若未达到,则添加新线程作为核心线程;若已达到,则添加新线程,不作为核心线程。至于addWorker是ThreadPoolExecutor中的核心方法,已在 Java 线程池之 ThreadPoolExecutor 源码分析 做了详细的解读,我们需要知道addWorker启动后,将会新建一个Worker线程,作为运行任务的线程,并且不断从任务队列中选取任务执行。Worker执行任务,是通过调用任务的run方法执行,我们看下ScheduledFutureTask的run方法是怎么实现的:
public void run() {
boolean periodic = isPeriodic();
// 判断当前状态能否运行
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 判断是否为周期性任务,若不是,则直接运行父类FutureTask的run方法
else if (!periodic)
ScheduledFutureTask.super.run();
// 执行任务,执行完成后,若成功执行,则重置下次运行时间,并且将任务再次加入到队列中
else if (ScheduledFutureTask.super.runAndReset()) {
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
这里首先判断是否能运行,若不能运行,则取消任务;然后判断是否为周期性任务,若不是,则运行父类FutureTask的run方法;若是周期性任务,则运行,并且重置任务状态为NEW;这里的FutureTask的run方法与runAndReset方法的区别在于,run方法就将任务状态更新,并且设置返回值,而runAndReset就重置任务状态,不设置返回值。
上面我们看到,任务如何添加到队列中去,下面我们将重点解读下,任务如何从队列中获取。
在ScheduledThreadPoolExecutor的构造方法中,我们看到,这里使用的队列是DelayedWorkQueue,DelayedWorkQueue是基于最小堆实现的,也就是说根元素是所有元素中最小的,每次获取元素只能从根元素获取。获取时,DelayedWorkQueue会判断根元素的延迟是否达到,若已达到,则开始运行,若未达到,则等待延迟时间后,再次尝试获取根元素,这样就能达到每次获取的都是最接近当前时间的任务。
关于DelayedWorkQueue的实现,我们将在下一篇文章中详细讲解。
问题总结
- ScheduledThreadPoolExecutor继承了ThreadPoolExecutor
- ScheduledThreadPoolExecutor执行任务时,先将任务添加到队列中,然后再取出,而不是执行运行
- ScheduledThreadPoolExecutor将任务包装成ScheduledFutureTask运行
- ScheduledThreadPoolExecutor使用DelayedWorkQueue队列,该队列作用时,将最近需要运行的任务,放在队列头部,以供提取运行