前言
之前主要是想了解Spring下的定时任务机制,但是在看了相关源码后,发现必须先要了解ScheduledThreadPoolExecutor,之后在阅读会更加简单,顾先去看了下这块的源码,发现其还是很有意思和学习的地方的。
ScheduledThreadPoolExecutor介绍
我们首先要知道ScheduledThreadPoolExecutor是JUC包下关于定时任务这块的,不知道大家跟我之前有没有一样的疑问,定时任务到底是如何执行的,在ScheduledThreadPoolExecutor就能满足你这个愿望,了解其运作原理。
源码解析
构造方法
这里的构造方法有4种,我拿一个最全的来距离:
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
- 可以看到构造函数的入参是只能传入核心线程数,线程工厂类和拒绝策略。在之前的文章中我们已经分析了ThreadPoolExecutor,而ScheduledThreadPoolExecutor是继承了这个类的,所以不去过多解释参数作用。
- 我们发现最大线程数设置的是最大的,其实这里这个参数是无用的,因为我们知道如果阻塞队列是无界的话,那么这个最大线程数参数就是无效的,而这DelayedWorkQueue队列就是这么一个无界队列。
DelayedWorkQueue类源码
首先这个类是是ScheduledThreadPoolExecutor的内部类,它的数据结构是小顶堆,小顶堆是什么?
最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。
怎么理解呢?说直白点就是一颗满二叉树,然后高度越高,值越小。
然后是为什么要这么设计呢?因为是定时任务呀,肯定是现在最短时间执行的排在越上面来进行读取。这边就大致讲一下,如果有问题的,欢迎交流。
下面直接来看源码,我们也知道阻塞队列在ThreadPoolExecutor的运作过程中会涉及到的是take和epoll方法,还有一个就是offer方法,我们就从这些方法入手。
offer方法源码
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);
}
if (queue[0] == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
- grow是扩容方法,比较的简单,这里不过多介绍,有兴趣一看就会
- setIndex方法设计到ScheduledFutureTask,这个后面会讲,就是把队列的索引序号冗余过去,方便删除的时候能不需要定位查询再删除
- siftUp就是插入值了,我们之后来看下源码
siftUp方法源码
private void siftUp(int k, RunnableScheduledFuture<?> key) {
while (k > 0) {
int parent = (k - 1) >>> 1;
RunnableScheduledFuture<?> e = queue[parent];
if (key.compareTo(e) >= 0)
break;
queue[k] = e;
setIndex(e, k);
k = parent;
}
queue[k] = key;
setIndex(key, k);
}
- 你品一下,再细品一下,其中(k - 1) >>> 1等于(k-1)/2,因为是小顶堆的数据结构,所以这个值等于k的父节点。
- 不知道你品出来没有,我这简单说下,就是如果是比父节点大的,就直接在指定位置插入,如果不是,就无限循环,把父节点的位置赋值到原先位置,然后向上查找。
take方法源码
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
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
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
- 之前也没有接触过阻塞队列是如何实现的,看完这段代码之后有了更深的了解
- 首先会获取队列中的第一个元素,如果是0的话,就会执行available.await()进行阻塞,等待调用 available.signal()方法
- OK,下面就是精髓了,也是我悟了一会才出来的精髓,这里和之前的阻塞队列不同,这里如果有值得话,会执行first.getDelay方法,这个作用是获取这个任务还需要多少时间延迟之后才能执行,如果<=0,则说明可以立即执行了,那么会调用finishPoll进行一些操作后,把队头元素返回。
- 如果还在延迟的话,注意,后面是精髓,这时leader是为null的,这个leader是干嘛的?我们之后就是来讲这个leader参数的作用。我们可以看到先是available.awaitNanos(delay)来等待对应的延迟时间,所以正常情况在等待了delay醒来之后,会把leader再置为null,再循环执行的时候就可以直接取出返回了。
- 这里我的说明不知道能不能理解,不理解的话欢迎交流下哦。
- epoll方法其实和take差不多,这里不做过多介绍,大家理解下take的情况下epoll也很好理解了。
ScheduledThreadPoolExecutor源码解析
我们知道这是继承ThreadPoolExecutor的,顾其使用方式也类似,我们来看下它的execute方法源码
execute方法源码
public void execute(Runnable command) {
schedule(command, 0, NANOSECONDS);
}
- 实际调用的是schedule源码
schedule方法源码
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
- decorateTask方法就是第二个传入参数,其中ScheduledFutureTask我们之后分析
- triggerTime就是获取下次执行的时间,
return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
- delayedExecute则是实际添加运行
delayedExecute方法源码
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();
}
}
- 检查线程池状态,如果是shutdown就直接拒绝
- 队列添加元素,然后会再确认一遍线程池状态再调用ensurePrestart方法
ensurePrestart方法源码
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
- 这里的意思是如果小于核心线程池时或者线程池设置为0,但是还是最少会有一条线程去执行逻辑
ScheduledFutureTask源码解析
这个类呢主要是用来实现延迟任务的,来看下继承图:
这里可以看到实现的三个方向是Runable、Future、Delayed,就能大概理解了他的功能。我们主要来看下关于Runable的实现,其他两个功能的实现也不难,有兴趣可以自己看下,有问题的话欢迎交流。
run方法源码
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);
}
}
- 这里我们可能会对ScheduledFutureTask的父类的run和runAndReset方法有点好奇,主要区别是是否吧result赋值进去
- setNextRunTime会根据time来计算下一次延迟执行的时间
- 这里简单总结下就是如果是不是周期性运行的,直接调用父类的run方法,然后可以通过get方法获取result值,如果是周期性的,会先计算下次执行时间, 然后再次把任务放入队列中。
个人总结
这篇定时任务总结的话,主要还是想要理清定时延迟任务时如何实现的,我们从头来开始理一下,首先是通过定义一个正常的线程池,这里的重点区别是放入的阻塞队列是内部类DelayedWorkQueue,然后回忆下这个类用到的offer、take、poll方法内容。OK,在使用中我们调用execute方法,实际上是封装成了一个内部的ScheduledFutureTask方法,然后通过其实现周期性运行。延迟的话也是通过ScheduledFutureTask实现了Delayed可获取到还需延迟的时间。
今日的知识图谱: