ScheduledThreadPoolExecutor 定时轮询任务实现
一、使用入口
可以在应用初始化时就加载好该线程池。
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(8);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
}, 1, 3, TimeUnit.SECONDS);//可以配置轮询任务的间隔时间
二、使用原理
ScheduledThreadPoolExecutor是基于线程池实现的,本人理解为初始化一个轮询任务线程池后,将任务放到线程池中执行,某个线程未到执行时间前处于阻塞状态,到了任务执行时间后变为运行状态。如果是轮询任务,则在本次执行完后,计算下一次需要执行的时间,插入到任务队列中对应的位置,等待下一次执行。
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对象
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
//处理sft这个对象,源码中其实未进行处理
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
//提交给线程池执行
delayedExecute(t);
return 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
//因为这里的定时任务是直接放到任务队列中,所以需要保证已经有worker启动了
ensurePrestart();
}
}
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
//如果worker的数量小于corePoolSize,那就启动一个worker,用来消费任务队列的任务
if (wc < corePoolSize)
addWorker(null, true);
//worker的数量为0也直接启动一个worker
else if (wc == 0)
addWorker(null, false);
}
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 检查任务队列是否为空
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
//检查当前线程池中的线程是否超过最大线程数
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
//否则,由于workerCount更改,CAS失败;重试内部循环
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
final ReentrantLock mainLock = this.mainLock;
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
mainLock.lock();
try {
int c = ctl.get();
int rs = runStateOf(c);
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // 检查线程是否可执行
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//执行该任务
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
到这里,任务已经放到线程池中去执行了,如果还需要延迟任务的执行时间,那么需要通过将任务队列变成延迟队列,worker不会马上获取到任务队列中的任务了。只有任务的时间到了,worker线程才能从延迟队列中获取到任务并执行。在ScheduledThreadPoolExecutor中,定义了DelayedWorkQueue类来实现延迟队列。DelayedWorkQueue内部使用了最小堆的数据结构,当任务插入到队列中时,会根据执行的时间自动调整在堆中的位置,最后执行时间最近的那个会放在堆顶。当worker要去队列获取任务时,如果堆顶的执行时间还没到,那么worker就会阻塞一定时间后才能获取到那个任务,这样就实现了任务的延迟执行。
周期任务的实现是在ScheduledFutureTask中,处理完本次任务后,会计算下一次的执行时间,然后重新放到任务队列中等待下一次执行。
public void run() {
//先判断任务是否周期执行
boolean periodic = isPeriodic();
//判断是否能执行任务
if (!canRunInCurrentRunState(periodic))
cancel(false);
//判断是否周期性任务
else if (!periodic)
//不是的话执行执行run方法
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
//如果是周期性任务,那就设置下一次的执行时间
setNextRunTime();
//重新将任务放到队列中,然后等待下一次执行
reExecutePeriodic(outerTask);
}
}
private void setNextRunTime() {
//根据peroid的正负来判断下一次执行时间的计算策略
//和timer的下一次执行时间计算策略有点像
long p = period;
if (p > 0)
time += p;
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();
}
}