一、简介
之前分析过ThreadPoolExecutor这个线程池的源码,Java线程池源码解析(ThreadPoolExecutor),今天来分析它的一个子类ScheduledThreadPoolExecutor,它可以用来实现定时任务,首先思考一下如果是你,你会怎么实现?
对于我,我想的是用一个最小堆存储任务,然后线程不断从这个最小堆里面拿任务,对于还没到时间的任务,让线程沉睡响应的时间即可。不过新加入一个任务,看这个任务是否会变成最小堆的堆顶,也就是延迟时间最小的任务,是的话就需要去唤醒线程,更新睡眠时间,
当我去看ScheduledThreadPoolExecutor的源码的时候,恰好它也是这么实现的,不过它是多线程实现的,更加复杂一点。接下来就来具体分析一下它的源码:
二、源码阅读
先来看看继承关系
它继承了ThreadPoolExecutor,并且实现了ScheduledExecutorService这个接口,所以它是基于ThreadPoolExecutor实现的,让我们来看看ScheduledExecutorService这个接口里面有哪些方法把。
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() 方法,以及一定时间间隔可多次执行的scheduleAtFixedRate() 和 scheduleWithFixedDelay() 方法。
来看一下它的构造方法把
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
其实就是调用了父类的构造方法,不过使用的是DelayedWorkQueue()这个类,也就是延迟的工作队列,这个在ThreadPoolExecutor中是没有的。
2.1 schedule(Runnable command, long delay,TimeUnit unit)
话不多说,直接来看schedule(Runnable command, long delay,TimeUnit unit)方法吧:
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
//参数校验
if (command == null || unit == null)
throw new NullPointerException();
//把我们的类转化为ScheduledFutureTask
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
//延迟执行
delayedExecute(t);
return t;
}
首先进行参数校验,然后把我们的任务封装成ScheduledFutureTask,再去执行,让我们看一下ScheduledFutureTask这个类;
private class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V> {
ScheduledFutureTask(Runnable r, V result, long ns)
//调用父类FutureTask的构造方法
super(r, result);
//设置超时时间
this.time = ns;
//period为0,说明只执行一次
this.period = 0;
this.sequenceNumber = sequencer.getAndIncrement();
}
}
public FutureTask(Runnable runnable, V result){
//把runnable接口转化为callable接口
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
让我们看一下triggerTime(delay, unit)是怎么实现的:
private long triggerTime(long delay, TimeUnit unit) {
//延迟时间小于0,设置为0
return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}
long triggerTime(long delay) {
//现在的时间戳加上延迟时间为唤醒的时间
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
看完任务的封装后,让我们看看线程池的执行过程吧,在delayedExecute(t)方法里面;
private void delayedExecute(RunnableScheduledFuture<?> task) {
//如果线程池关闭了,则执行线程池拒绝策略
if (isShutdown())
reject(task);
else {
//线程池正常运行,添加任务到延迟队列
super.getQueue().add(task);
//再次检查线程池的状态,线程池要关闭,直接把任务从延迟队列中拿出来,并取消任务
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);
//到这里说明corePoolSize==0,但是保证创建一个线程去执行任务。
else if (wc == 0)
addWorker(null, false);
}
addWorker()之前在线程池那篇分析过了,这里就不分析了,主要就是新建然后运行线程,然后线程会去延迟队列take任务。看看DelayedWorkQueue这个类中take()具体的实现逻辑;
public RunnableScheduledFuture<?> take() throws InterruptedException {
// 取任务和放任务用的是同一把,所以取任务和放任务不会同时进行
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
//拿到堆顶的元素
RunnableScheduledFuture<?> first = queue[0];
//如果说为空,也就是队列中没有任务,那么就在available的等待队列中,进行等待
if (first == null)
available.await();
else {
//获取任务的延迟时间
long delay = first.getDelay(NANOSECONDS);
//时间已经到了,取出任务,并且调整堆
if (delay <= 0)
return finishPoll(first);
first = null; // don't retain ref while waiting
// 如果延迟时间还没到,并且leader不为空
// 稍后应该由leader去执行这个任务,自己去等待
if (leader != null)
available.await();
else {
//leader为空,把当前线程设置为leader,等待相应的延迟
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
//当等待的时间到了,说明这个线程可以拿到堆顶的任务去执行,所以把leader置空
//让其它等待的线程当leader
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 如果没有设置leader而且队列中不为空,那么需要唤醒在available上等待的线程
// 让其中一个线程当leader,不然这些线程只会一直阻塞下去
// 因为队列中任务不为空,加入一个任务,不会唤醒线程
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
从队列中取任务的逻辑,我觉得最难的是leader这个点,ScheduledThreadPoolExecutor把线程分为两种,一种是leader,一种是一般的线程,对于leader,它只会等待堆顶的最少延时时间任务的时间,对于一般线程,会无限期的等待在等待队列中, 直到被唤醒。这其实是为了避免堆顶时间到了,多个线程同时被唤醒,然后去争抢锁,去队列中拿任务。
对于ScheduledThreadPoolExecutor,对于不是立即执行的任务,只有leader才会拿到队列的任务,拿到队列的任务之后,然后让出leader这个位置,让其他线程有机会成为leader。
当一线程拿到任务之后,finally是这样执行的:
- 如果等待的线程没有设置leader,并且任务队列为空,一个线程拿到任务后不需要唤醒线程去设置leader,因为加入任务的时候会唤醒线程让其中一个变成leader;
- 如果等待的线程没有设置leader而且队列中不为空,那么一个线程拿到任务后需要唤醒在available上等待的线程,让其中一个线程当leader,不然这些线程可能一直阻塞下去,因为队列中任务不为空,不加入一个延迟时间比堆顶小的任务,是不会唤醒线程的;
之前说到添加任务到延迟队列,看看具体怎么实现的:
public boolean add(Runnable e) {
return offer(e);
}
public boolean offer(Runnable x) {
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
final ReentrantLock lock = this.lock;
//保证并发加入队列的线程安全
lock.lock();
try {
//获取任务个数
int i = size;
// 任务达到数组的长度,就扩容
if (i >= queue.length)
grow();
size = i + 1;
//如果原来队列中没有任务
if (i == 0) {
//放在堆顶
queue[0] = e;
//记录任务的下标,这样可以通过下标快速的取消任务
setIndex(e, 0);
} else {
//根据延迟的时间调整堆
siftUp(i, e);
}
// 如果是当前任务在堆顶,要么是第一个任务,要么是延迟时间最短的任务
// 那么需要唤醒阻塞在条件队列availabl上的线程,并且把leader置为空
if (queue[0] == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
放入队列的任务,如果当前任务在堆顶,说明要么是第一个任务,要么是加入的任务是延迟时间最短的任务,这时候需要将leader置为空,并且去唤醒线程,这样会由一个新的leader出现,新的leaderl的等待是堆顶任务的等待时间。
2.2 scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)
我们再来看看scheduleWithFixedDelay()方法:
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();
// 注意这里是period=-delay<0
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;
}
其实跟之前的schedule()方法是一样的,那么它是怎么实现固定间隔再次执行任务的呢?
其实是在ScheduledFutureTask的run()方法里面:
public void run() {
// 是否只执行一次
boolean periodic = isPeriodic();
// 线程池关闭,取消任务
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 只执行一次,也就是调用schdule时候
else if (!periodic)
ScheduledFutureTask.super.run();
// 定时执行
else if (ScheduledFutureTask.super.runAndReset()) {
// 设置下次的执行时间
setNextRunTime();
// 重新加入该任务到delay队列
reExecutePeriodic(outerTask);
}
}
private void setNextRunTime() {
long p = period;
//fixed-rate类型任务,拿的上一次任务开始的时间+时间间隔
if (p > 0)
time += p;
//fixed-delay类型任务,拿的任务完成的时间+时间间隔
else
time = triggerTime(-p);
}
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
// 线程池可运行
if (canRunInCurrentRunState(true)) {
// 把任务加到队列中去
super.getQueue().add(task);
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
对于schedule()方法,只会运行一次,对于scheduleWithFixedDelay()方法,运行一次之前会进行重置,把任务再次加入到延时队列中去,实现多次运行。scheduleAtFixedRate()同样的原理,主要区别就是scheduleAtFixedRate 表示上一个任务开始到下一个任务开始的时间,scheduleWithFixedDelay 则表示上一个任务结束到下一个任务开始的时间,就不多说了。
三、总结
ScheduledThreadPoolExecutor 的实现原理,其内部使用的 DelayedWorkQueue来存放具体任务,在offer的时候会按照延迟时间的从小到大的顺序插入到队列当中去,对于线程拿任务,都是拿堆顶的任务,然后看是任务是否到了该执行的时间了,到了立即执行,没到就需要设置一个leader去等待相应的时间。对于周期性的任务,每次任务执行完之后再计算出下一次的运行时间,然后再重新插入到队列中。在ScheduledFutureTask 的run方法中完成。