线程池ScheduledThreadPoolExecutor原理剖析
文章目录
文章创作参考于 Java并发-----第9章 Java并发包中ScheduledThreadPoolExecutor原理剖析
1. 介绍
这是一个可以在制定一定延迟时间后或者定时进行任务调度的线程池.
2. 结构
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor并实现了ScheduledExecutorService接口
线程池(任务)队列是DelayedWorkQueue,和DelayedQueue(每一个节点有一个过期时间,队列首元素最快过期,出队时如果不过期短暂挂起,自旋判断)类似,是一个延迟队列
ScheduledFutureTask是具有返回值的任务,集成自FutureTask
FutureTask内部有一个变量state用来表示任务的状态
状态 | 含义 |
---|---|
NEW | 0,初始状态 |
COMPLETING | 1,执行中 |
NORMAL | 2,正常运行结束 |
EXCEPTIONAL | 3,运行中异常 |
CANCELLED | 4,任务被取消 |
INTERRUPTING | 5,任务正在被中断 |
INTERRUPTING | 6,任务已经被中断 |
任务状态转换路径:
- NEW-> COMPLETING -> NORMAL :任务正常执行
- NEW -> COMPLETING -> EXCEPTIONAL :执行出现异常
- NEW -> CANCELLED : 任务被取消
- NEW -> INTERRUPTING -> INTERRUPTING : 任务被中断
3. 原理剖析
(1). schedule()方法
该方法提交一个延迟执行的任务,任务从提交时间算起延迟单位为unit的delay时间后开始执行,任务只会执行一次.
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
// 参数校验
if (command == null || unit == null)
throw new NullPointerException();
// 装饰任务,将Runnable类型的任务封装为ScheduledFutureTask对象t
RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit)));
// 添加到延迟队列
delayedExecute(t);
return t;
}
(2). scheduleWithFixedDelay()方法
该方法的作用是当任务执行完毕后,让其延迟固定时间后再次运行.
扫描二维码关注公众号,回复:
9177353 查看本文章
当任务执行完毕后,让其延迟固定的时间再次执行.
任务会一致重复执行到任务运行中出现了异常,或者被取消,被关闭线程池.
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();
// 封装为ScheduledFutureTask对象,延迟执行任务
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;
}
(3). scheduleAtFixedRate()方法
当任务执行完毕后,让其延迟固定的时间再次执行,这个动作会一直重复到任务运行中抛出异常,被取消或者线程池被关闭.
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();
// 这里period不是负的,所以在后面的调用当中,设置的时间是time+=p,而不是time=triggerTime(-p)
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
(4). 共同点和不同点
总结如下:
- scheduleAtFixedRate和scheduleWithFixedDelay都是在初始delay后开始执行,
- scheduleAtFixedRate每次任务执行的时间理论上是固定的,不算任务执行的时间,依次是initialDelay、initialDelay+period、 initialDelay + 2 * period定点执行任务,如果一次任务的执行消耗时间过多,将延后下个任务执行;
- 在这点上scheduleWithFixedDelay不一样,scheduleWithFixedDelay是下次任务的执行是在上次任务执行完成后的delay时间后开始。
- 两者在任务抛异常后都会终止任务执行,另外通过cancel也可以终止任务执行。
(5). ScheduledFutureTask的构造
// ScheduledFutureTask的构造,其中先调用了父类FutureTask的构造,然后设置时间为绝对时间
ScheduledFutureTask(Runnable r, V result, long ns) {
super(r, result);
this.time = ns;
this.period = 0;
this.sequenceNumber = sequencer.getAndIncrement();
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW;// 初始状态为NEW
}
// 将任务添加到延迟队列,队首是最快过期的元素
private void delayedExecute(RunnableScheduledFuture<?> task) {
// 如果线程池关闭了,执行拒绝策略
if (isShutdown())
reject(task);
else {
// 添加到延迟队列
super.getQueue().add(task);
// 再次检查状态,如果线程池被关闭了,从延迟队列中溢出刚刚添加的任务
// 因为任务可能已经在执行了,所以调用cancel()取消任务
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
// 确保至少一个线程在处理问题
ensurePrestart();
}
}
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
// 增加到核心线程
if (wc < corePoolSize)
addWorker(null, true);
// 初始化时设置核心线程数为0,那么至少初始化一个线程
else if (wc == 0)
addWorker(null, false);
}
(6). 任务的执行
// ScheduledFutureTask类的run方法,执行Worker线程调用的具体任务
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);
}
}
// 判断任务是否只执行依次,period变量在创建ScheduledFutureTask对象时传入
public boolean isPeriodic() {
return period != 0;
}
//父类FutureTask的run方法
public void run() {
// 如果任务状态不是NEW 直接返回
// 如果任务状态是NEW 但是 CAS设置持有者为当前线程失败 直接返回
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
// 执行任务,这里就说明真正的任务就是Callable中call()
result = c.call();
ran = true;
} catch (Throwable ex) {
// 任务出现异常
result = null;
ran = false;
// 与set方法类似,只不过最后修改状态为EXCEPTIONAL
setExjavaception(ex);
}
// 任务正常执行完毕,设置其返回值
if (ran)
// 使用CAS将任务状态设置为COMPLETING
// 只有一个线程会成功
// 然后在设置为NORMAL状态,意为任务正常结束
set(result);
}
} finally {
runner = null;
int s = state;
// 任务被中断时执行
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
//父类FutureTask的runAndReset方法,和上面的run方法类似
protected boolean runAndReset() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return false;
boolean ran = false;
int s = state;
try {
Callable<V> c = callable;
if (c != null && s == NEW) {
try {
c.call();
ran = true;
// 这里正常结束后没有设置任务的状态,为了让任务成为可执行的任务
} catch (Throwable ex) {
setException(ex);
}
}
} finally {
runner = null;
s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
// 当前任务执行完毕并且任务状态为NEW 返回true
return ran && s == NEW;
}
// 设置下一次执行任务的时间
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));
}
4. 总结
ScheduledThreadPoolExecutor可以对进入线程池的任务进行时间上的调度,有三种可选的执行方式:
- schedule()方法:单次的在到一定时间之后执行任务
- scheduleWithFixedDelay()方法:多次以某一个时间间隔重复的在一段时间之后执行某任务
- scheduleAtFixedRate()方法:与2类似,但计算时间间隔不考虑程序运行的时间