由于比较好奇java的定时任务线程池是怎么实现定时执行的功能的。就研究了下ScheduledThreadPoolExecutor的源码。ScheduledThreadPoolExecutor继承ThreadPoolExecutor,也是使用ThreadPoolExecutor的线程池基本功能的。
如果不了解线程池基本原理的,可以先去了解一下线程池的源码,了解下线程池的工作方式,Worder, BlockingQueue 是怎么协调工作的,核心线程数、最大线程数都意味着什么。因为定时任务线程池也是基于普通线程池来实现的。
定时任务线程池构造时的关键属性
ScheduledThreadPoolExecutor的四个构造方法提供传入:核心线程数,Thread工厂类,拒绝策略三个参数。最终是调用其父类ThreadPoolExecutor的构造方法。
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
调用ThreadPoolExecutor构造函数的参数:
核心线程数(corePoolSize):用户指定
最大线程数(maximumPoolSize):Integer.MAX_VALUE 可以理解为无限大
线程等待时间(keepAliveTime):0
阻塞队列 (workQueue):DelayedWorkQueue 延时阻塞队列
threadFactory和handler :用户指定
应用的话主要有这4个方法:
/**
delay时间之后执行callable任务,是一次性的,执行完一次就结束了。
*/
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay,
TimeUnit unit)
/**
delay时间之后执行command任务,是一次性的,执行完一次就结束了。
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit)
/**
initialDelay时间之后执行command任务
第二次的执行时间是 initialDelay + period
第三次的执行时间是 initialDelay + period * 2
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit)
/**
initialDelay时间之后执行command任务
第二次的执行时间是 第一次任务结束时间 + delay
第三次的执行时间是 第二次任务结束时间 + delay
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit)
下面我们来看看这几个方法的功能是怎么实现的。
定时任务实现的关键就是这个:DelayedWorkQueue 延时阻塞队列
延时阻塞队列内部使用数组(queue)来实现的堆的功能,将队列中的元素按照延时大小排序,类似堆排序中的堆的构造过程。不了解的可以先去了解一下堆排序。这里不展开讨论。总之,队列出队操作总是返回这样的任务: 任务开始执行的时间距离当前时间最短的任务。这是出队的代码,因为quene数组已经排好序,并且第一个元素距离当前时间最短,所以每次出队都返回queue[0]。
first.getDelay(NANOSECONDS): 是获取任务的当前时间和延时时间(即任务从提交之后多久开始执行)的差值。
如果 > 0:也就是还没有到任务的执行时间 return null, 这样的话就可以保护没到执行时间的任务不会出队被执行。
如果 <= 0:将任务出队去执行,之后还要调整堆的结构,以使堆保持有序。
public RunnableScheduledFuture<?> poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
RunnableScheduledFuture<?> first = queue[0];
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return finishPoll(first);
} finally {
lock.unlock();
}
}
public long getDelay(TimeUnit unit) {
return unit.convert(time - now(), NANOSECONDS);
}
线程池的基本操作是把任务(Runnable对象)封装成了Worker对象,Worker对象不停的从队列中取排队的任务去执行,不同的队列取数逻辑不一样。那么就会有个疑问,这里出队操作在任务没到期之前一直是返回null,那么线程池中的线程得不到任务执行,不就退出了吗。其实线程池有个策略,如果尚有任务再队列中,并且线程池没有停止的话,会启动一个新的Worker,继续从队列获取任务。保护线程池不会存在尚有任务,但是没有运行线程的情况发生。
下面是Worker从队列获取任务执行的代码:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// task = getTask() 就是从队列获取任务
// 如果初始任务和从队列中拿到的任务都为空,则退出while循环
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 像普通方法一样,执行Runnable对象的run方法,调用run方法并不会创建新的线程
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 执行线程退出任务,再这里判断,如果队列中尚有任务,则新建worker
processWorkerExit(w, completedAbruptly);
}
}
/**
执行线程退出任务
*/
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
tryTerminate();
int c = ctl.get();
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 在这里新建了一个worker
addWorker(null, false);
}
}
这样就可以实现任务提交之后,delay时间才开始执行的功能。不到时间的话,worker获取队列数据一直为空,退出之后再新建一个worker, 继续去队列获取任务。直到可以取出来到期的任务为止。我感觉这里有个性能的问题,这种自旋式的获取队列任务的方式有点浪费性能,因为有corePoolSize个线程在自旋取数。不知道是不是我理解的有问题,也希望各位可以评论指教一二。
以上只是实现了延时第一次执行任务的功能,那么还有一个功能就是可以间隔特定时间执行任务,这个是怎么实现的呢?
其实也是依赖以上的功能,只不过再任务执行结束之后,重置了一下下次执行时间,再把该任务仍回线程池的队列中。
那么任务执行结束之后都做了什么呢。
public void run() {
// 判断是单次执行的任务,还是需要定时反复多次执行的任务
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic) // 单次执行的任务
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) { // 多次执行的任务
setNextRunTime(); // 重置下次执行时间
reExecutePeriodic(outerTask); // 把当前任务重新仍回线程池
}
}
/**
重置下次执行时间
回顾之前说的,定时任务线程池有四种常用的使用方式,前两次单次执行任务的先不说,后两次 反复执行的方法:scheduleAtFixedRate 和 scheduleWithFixedDelay 的区别是前者是考虑的任务开始时间之间的间隔,后者计算的是前一个任务的结束时间和后一个任务的开始时间的间隔,这两个功能主要是就是通过这个重置时间方法实现的。scheduleWithFixedDelay 再构造的时候后,传进来的period是负数
这里判断如果是负数的话,就调用triggerTime获取时间,triggerTime是在当前时间 + 间隔时间。
如果是正数的话,就是任务开始的时间 + 间隔时间
*/
private void setNextRunTime() {
long p = period;
if (p > 0)
time += p;
else
time = triggerTime(-p);
}
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
/**
把当前任务重新仍回线程池
*/
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
if (canRunInCurrentRunState(true)) {
super.getQueue().add(task); // 将任务入队
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
ensurePrestart(); // 确保有一个线程再运行,不会出现队列中尚有任务,没有线程的情况
}
}
回顾之前说的,定时任务线程池有四种常用的使用方式,前两次单次执行任务的先不说,后两次反复执行的方法:scheduleAtFixedRate 和 scheduleWithFixedDelay 的区别是前者是考虑的任务开始时间之间的间隔,后者计算的是前一个任务的结束时间和后一个任务的开始时间的间隔,这两个功能主要是就是通过这个重置时间方法实现的。scheduleWithFixedDelay 再构造的时候,传进来的period是负数,这里判断如果是负数的话,就调用triggerTime获取时间,triggerTime是在当前时间 + 间隔时间。如果是正数的话,就是任务开始的时间 + 间隔时间