前言
ScheduledThreadPoolExecutor是用于处理定时任务的线程池,它继承ThreadPoolExecutor,拥有线程池的基础功能;而其定时处理的能力是来自于它定义的任务阻塞队列DelayedWorkQueue。通过上一篇文件得知线程池的运行原理,以及任务队列的作用,想要了解的可以移步线程池ThreadPoolExecutor原理解析
ScheduledThreadPoolExecutor源码解析
首先通过其入口函数分析schedule()函数
// 延迟多长时间执行
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
// 构建包装task, decorateTask()默认返回第二个参数
RunnableScheduledFuture<Void> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit),
sequencer.getAndIncrement()));
// 执行task
delayedExecute(t);
return t;
}
// 以指定速率循环执行
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0L)
throw new IllegalArgumentException();
// 和上面的区别:构造函数增加了period参数
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period),
sequencer.getAndIncrement());
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
RunnableScheduledFuture的参数如下:
cammand: 真正执行task的
result: 执行的结果
tirggerTime: 触发task的事件
period: 循环执行的速率
sequenceNumber:触发task的序号
下面来看RunnableScheduledFuture的构造函数如下:
ScheduledFutureTask(Runnable r, V result, long triggerTime, period, long sequenceNumber) {
super(r, result);
this.time = triggerTime;
this.period = period;
this.sequenceNumber = sequenceNumber;
}
下面继续看delayedExecute()
private void delayedExecute(RunnableScheduledFuture<?> task) {
// 如果当前线程池处于SHUTDOWN状态,执行reject策略
if (isShutdown())
reject(task);
else {
// 否则,将task添加到task阻塞队列中
super.getQueue().add(task);
// 检查当前状态是否可以运行
// 这里做了特殊检验:在SHUTDOWN状态,如果用户没有要求在SHUTDWON状态还可以接收新的task,那么下面会删除task
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
// 调用ThreadPoolExecutor的ensurePrestart()执行task
ensurePrestart();
}
}
通过之前ThreadPoolExecutor的源码分析可知,在SHUTDOWN状态不会接收新的task;因此在这里添加task之后,做了第二次的校验。
在SHUTDOWN状态,如果用户没有显示指定可以在SHUTDOWN状态接收新的task,那么会将task删除掉的。
boolean canRunInCurrentRunState(boolean periodic) {
//continueExistingPeriodicTasksAfterShutdown和executeExistingDelayedTasksAfterShutdown 默认均为false
return isRunningOrShutdown(periodic ?
continueExistingPeriodicTasksAfterShutdown :
executeExistingDelayedTasksAfterShutdown);
}
//isRunningOrShutdown()是ThreadPoolExecutor的函数
final boolean isRunningOrShutdown(boolean shutdownOK) {
int rs = runStateOf(ctl.get());
return rs == RUNNING || (rs == SHUTDOWN && shutdownOK);
}
//通过上面可知,只有当continueExistingPeriodicTasksAfterShutdown为true,才允许在SHUTDOWN状态,允许接收新task
ensurePrestart()方法如下:
void ensurePrestart() {
// 获取当前线程数量
int wc = workerCountOf(ctl.get());
// 如果线程数小于corePoolSize,创建线程执行
if (wc < corePoolSize)
addWorker(null, true);
//下面是对应corePoolSize为0的情况
else if (wc == 0)
addWorker(null, false);
}
addWorker()方法就不进行解析了,上面的链接中,可以查看。执行完addWorker()后,会执行runWorker()复用coreSize的线程执行其他task。
通过上面的分析,我们并没有发现时间调度的代码,其实这部分功能主要是通过其阻塞task队列DelayedWorkQueue实现,下面进行分析。
最后,来看运行的方法run()方法
public void run() {
boolean periodic = isPeriodic(); // period是否大于0
// 是否允许在SHUTDOWN状态,执行task;默认不允许,会将task关闭
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 如果不是period任务(按照指定时间间隔执行),直接执行
else if (!periodic)
super.run();
// 否则是定时任务,需要将task先reset
else if (super.runAndReset()) {
// 设置一次执行的时间,在当前时间基础加period时间
setNextRunTime();
// 重新执行任务
reExecutePeriodic(outerTask);
}
}
// 更新下一执行的时间
private void setNextRunTime() {
long p = period;
if (p > 0)
time += p;
else
time = triggerTime(-p);
}
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
// 判断当前state是否可以执行,RUNNING状态,或者SHUTDOWN状态用户手动设置continueExistingPeriodicTasksAfterShutdown为true
if (canRunInCurrentRunState(true)) {
// 允许执行,重新将task添加到队列中
super.getQueue().add(task);
// 添加完,状态改变,删除任务,并关闭任务
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
// 否则,小于corePoolSize,创建新的线程
else
ensurePrestart();
}
}
通过上面的分析,可以知道按照指定速率执行的任务是如何实现的。
DelayQueue时间调度实现
首先,我们知道线程池的阻塞队列是BlockQueue类型,因此DelayQueue继承自BlockQueue;因此,先分析BlockQueue。
BlockQueue
BlockQueue是一个接口,jdk总共提供了5个实现,分别实现不同的阻塞队列,下面给出五种队列及其功能。
- ArrayBlockingQueue : 存储数据结构为数组,所包含的对象是按照FIFO排序的,可以设置其最大尺寸。
- LinkedBlockingQueue:存储数据结构为链表,同样是FIFO排序,也可以设置其最大尺寸
- PriorityBlockingQueue: 存储数据结构为数组,它的排序是通过构造函数的Comparator决定,它可以定义task的执行优先级,也可以设置最大尺寸。
- SynchronousQueue:类似于生产者-消费者模型,它必须是存储和获取交替进行。
BlockQueue关键api说明:
插入数据:
- add(e): 如果queue不能容纳当前对象,会抛出异常
- offer(e): 如果queue不能容纳当前对象,返回false,也就是插入失败
- put(e): 如果queue不能容纳当前对象,阻塞线程,直到queue有空余空间,再插入
- offer(2, time, unit): 如果queue不能容纳当前对象, 阻塞线程time时间内,如果有空余空间,则插入,返回true;否则返回false
移除数据
- remove(): 如果queue为空,那么会抛出异常;否则返回数据
- poll(): 如果queue为空,那么返回null;否则返回数据
- take(): 如果queue为空,那么阻塞线程直到queue不为空,取出数据返回。
- poll(time, unit): 如果queue为空,那么zuse线程time时间内,如果有数据加入,那么直接返回;否则返回null。
DelayedWorkQueue
DelayedWorkQueue是继承BlockQueScheduledThreadPoolExecutor实现的阻塞队列。它的数据结构是堆,如果不熟悉堆的,推荐一篇文章纸上谈兵:堆
下面来看,DelayedWorkQueue的添加数据的api实现
// 所有的插入数据的方法,实现都是调用的下面这个方法
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;
// 如果当前queue为空,直接将堆顶设置为当前对象,并且设置当前对象的heapIndex为堆顶index0
if (i == 0) {
queue[0] = e;
setIndex(e, 0);
} else {
//往堆中当前index添加对象,并对堆结构优先级,进行调整元素,向上进行调整
siftUp(i, e);
}
// 如果堆顶就是当前对象,那么通知阻塞的线程,当前queue中已经有数据了。
if (queue[0] == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
public void put(Runnable e) {
offer(e);
}
public boolean add(Runnable e) {
return offer(e);
}
public boolean offer(Runnable e, long timeout, TimeUnit unit) {
return offer(e);
}
下面来看看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;
}
// 最后得出的k的位置就是当前对象的位置
queue[k] = key;
setIndex(key, k);
}
下面来看,取数据的api:
public RunnableScheduledFuture<?> poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 取堆顶元素
RunnableScheduledFuture<?> first = queue[0];
// 如果堆顶为null或者执行时间还没到,会返回null;否则返回当前task,并调整堆
return (first == null || first.getDelay(NANOSECONDS) > 0)
? null
: finishPoll(first);
} finally {
lock.unlock();
}
}
private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
// 数量减一
int s = --size;
// 取出堆尾元素
RunnableScheduledFuture<?> x = queue[s];
queue[s] = null;
if (s != 0)
// 将堆尾元素放到堆顶,并调节堆的结构
siftDown(0, x);
// 将headpIndex设置为-1,也就是当前task不在堆中了
setIndex(f, -1);
return f;
}
通过上面,数据是按照堆的结构存储,也就是根据执行的时间进行排序,每次去也是取堆顶元素。这样能保证task按照执行时间进行排序。
并且,取到task,但是task的执行时间还没有到,那么会返回null;上篇文件讲解线程池的原理,getTask()函数里面,在workQueue返回的null的情况,只要线程数没有达到corePoolSize,就会一直循环,直到有task可以执行。
我们直到getTask()主要使用的是take()方法,如果超过corePoolSize,会使用poll(time,unit),源码如下:
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
//如果当前queue为空,那么会等有新的task加入,然后下次循环执行
if (first == null)
available.await();
else {
// 取还需延迟多长时间
long delay = first.getDelay(NANOSECONDS);
// 如果为0,直接返回task,并调整堆
if (delay <= 0L)
return finishPoll(first);
first = null; // don't retain ref while waiting
// 特殊情况
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
// 设置leader
leader = thisThread;
try {
// 延迟delay时间,重新获取task
available.awaitNanos(delay);
} finally {
// 如果线程没有改变,将leader清空
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
public RunnableScheduledFuture<?> poll(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
// 取堆顶元素
RunnableScheduledFuture<?> first = queue[0];
// queue为空
if (first == null) {
// keepAliveTime(nanos就是keepAliveTime时间)剩余时间为0,直接返回null
if (nanos <= 0L)
return null;
else
// 否则等待keepAliveTime时间
nanos = available.awaitNanos(nanos);
} else { // 如果堆顶不为null
// 获取task的延迟时间
long delay = first.getDelay(NANOSECONDS);
// delay时间到了执行时间,执行task
if (delay <= 0L)
return finishPoll(first);
// keepAliveTime时间到了,直接返回null
if (nanos <= 0L)
return null;
first = null; // don't retain ref while waiting
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
long timeLeft = available.awaitNanos(delay);
nanos -= delay - timeLeft;
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
通过上面可以得知,schedule操作是通过DelayWorkQueue的阻塞操作实现。
总结
- ScheduledThreadPoolExecutor继承了线程池原有的功能,线程复用和关闭的功能
- 在线程池原有的功能基础上,通过阻塞task队列实现schedule功能