ScheduledThreadPoolExecutor类图如下
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor并实现了ScheduledExecutorService接口。线程池队列是DelayedWorkQueue,是一个延迟队列。
ScheduledFutureTask是具有返回值的任务,继承自FutureTask。FutureTask的内部有一个变量state用来表示任务状态,所有状态为:
private volatile int state;
/**
* 初始状态
*/
private static final int NEW = 0;
/**
* 执行中的状态
*/
private static final int COMPLETING = 1;
/**
* 正常运行结束状态
*/
private static final int NORMAL = 2;
/**
* 运行中异常
*/
private static final int EXCEPTIONAL = 3;
/**
* 任务被取消
*/
private static final int CANCELLED = 4;
/**
* 任务正在被中断
*/
private static final int INTERRUPTING = 5;
/**
* 任务已经被中断
*/
private static final int INTERRUPTED = 6;
可能的任务状态转换路径为
NEW → COMPLETING → NORMAL
NEW → COMPLETING → EXCEPTIONAL
NEW → CANCELED
NEW → INTERRUPTING→ INTERRUPTED
ScheduledFutureTask内部还有一个变量period用来表示任务的类型,任务类型如下:
下面是ScheduledThreadPoolExecutor的构造函数
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), handler);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
由构造函数可知线程队列是DelayedWorkQueue。
该方法的作用是提交一个延迟执行的任务,任务从提交时间起延迟单位为unit的delay时间后开始执行。提交的任务不是周期性任务,任务只会执行一次。
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
// 参数检验
if (command == null || unit == null)
throw new NullPointerException();
// 任务转换,可以继承,重写decorateTask,decorateTask原方法什么都没做
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
// 添加任务到延迟队列
delayedExecute(t);
return t;
}
着重说一下上述代码中的任务转换,decorateTask的作用是把提交的command任务转换为
ScheduledFutureTask。ScheduledFutureTask实现了long getDelay(TimeUnit unit)和
int compateTo(Delayed other)方法。
triggerTime(delay, unit)方法将延迟时间转换为绝对时间,也就是把当前时间的纳秒加上延迟纳秒后的long型值。
ScheduledFutureTask的构造函数如下
ScheduledFutureTask(Runnable r, V result, long ns) {
// 调用父类的构造方法
super(r, result);
this.time = ns;
// period为0,说明为一次性任务
this.period = 0;
// 序列号
this.sequenceNumber = sequencer.getAndIncrement();
}
在构造函数内部首先调用父类FutureTask的构造函数,代码如下:
public FutureTask(Runnable runnable, V result) {
// 通过适配器把runnable转换为callable
this.callable = Executors.callable(runnable, result);
// 设置当前任务状态为NEW
this.state = NEW; // ensure visibility of callable
}
在FutureTask构造函数里面,把任务类型转换为Callable类型后,被保存到了变量this.callable里面,并设置任务状态为NEW。
ScheduledFutureTask构造函数内部设置time为绝对时间,long getDelay(TimeUnit unit)如下
public long getDelay(TimeUnit unit) {
// 计算过期时间,装饰后的时间-当前时间,即将过期剩余时间
return unit.convert(time - now(), NANOSECONDS);
}
campareTo(Delayed other)方法的代码如下:
public int compareTo(Delayed other) {
if (other == this) // compare zero if same object
return 0;
if (other instanceof ScheduledFutureTask) {
ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else if (sequenceNumber < x.sequenceNumber)
return -1;
else
return 1;
}
long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}
compareTo的作用是加入元素到延迟队列后,在内部建立或者调整堆时会使用该元素的compareTo方法与队列里面其他元素进行比较,让最快过期的元素放到队首,所以无论什么时候向队列里面添加元素,队首的元素都是最快过期的元素。
将任务添加到延迟队列,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();
}
}
这里解释一下,为什么要检查两次线程池是否被关闭了?
第一次检查线程池是否被关闭了,是因为需要关闭就执行线程池的拒绝策略,没有关闭的话,就将任务添加到延迟队列。
第二次检查是任务添加完成后,如果已经关闭,那么从延迟队列里面删除刚才添加的任务,由于此时有可能线程池中的线程已经从延迟队列里面获取了该任务,也就是该任务在执行了,所有还需要条用任务的cancle方法取消任务。
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
// 线程个数小于核心线程数,就增加一个核心线程数
if (wc < corePoolSize)
addWorker(null, true);
// 如果初始化corePoolSize==0,则添加一个线程
else if (wc == 0)
addWorker(null, false);
}
由源码可以看出,ensurePrestart()就是添加线程,确保至少有一个线程在处理任务。
前面分析了如何向延迟队列添加任务,下面我们来看线程池的线程获取并执行任务。
类比一下,ThreadPoolExecutor里面,具体执行任务的线程是worker线程,worker线程
调用具体任务的run方法来执行,由于这里的任务是ScheduledFutureTask,看看ScheduledFutureTask的run方法
/**
* Overrides FutureTask version so as to reset/requeue if periodic.
*/
public void run() {
// 判断是否执行一次
boolean periodic = isPeriodic();
// 取消任务
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 只执行一次,调用schedule方法的时候
else if (!periodic)
ScheduledFutureTask.super.run();
// 定时执行
else if (ScheduledFutureTask.super.runAndReset()) {
// 设置时间
setNextRunTime();
// 重新加入该任务到delay队列
reExecutePeriodic(outerTask);
}
}
/**
* 判断执行一次还是重复执行
*/
public boolean isPeriodic() {
return period != 0;
}
/**
* 判断当前任务是否应该被取消 executeExistingDelayedTasksAfterShutdown(默认true)
* executeExistingDelayedTasksAfterShutdown默认为true,表示当其他线程调用shutdown
* 命令关闭了线程池后,当前任务还是要执行,如果为false,则当前任务要被取消
* 执行一次的时候,periodic为false
*/
boolean canRunInCurrentRunState(boolean periodic) {
return isRunningOrShutdown(periodic ?
continueExistingPeriodicTasksAfterShutdown :
executeExistingDelayedTasksAfterShutdown);
}
private void setNextRunTime() {
long p = period;
if (p > 0) // fixed-rate类型
time += p;
else // fixed-delay类型
time = triggerTime(-p);
}
/**
* 是ThreadPoolExecutor中的方法
* @param shutdownOK 表示线程池能在SHUTDOWN状态下运行
*/
final boolean isRunningOrShutdown(boolean shutdownOK) {
// 线程状态
int rs = runStateOf(ctl.get());
// 当线程是RUNNING状态,或者是SHUTDOWN状态且shutdownOK也为true
return rs == RUNNING || (rs == SHUTDOWN && shutdownOK);
}
如果periodic为false,则调用父类FutureTask的run方法执行具体任务
public void run() {
// a
// 如果任务状态不是NEW则直接返回
// 如果当前状态是NEW,但是使用CAS设置当前任务的持有者为当前线程失败也返回
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
// 调用callable的call()执行具体任务
try {
Callable<V> c = callable;
// 再次判断任务状态是否为NEW,是为了避免执行a代码块之后,其他线程修改了任务状态
// 比如取消了任务
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 发生异常,运行失败
setException(ex);
}
if (ran)
// 运行成功,修改任务状态
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
protected void set(V v) {
// 如果当前任务状态为NEW,则设置为COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
// 设置当前任务状态为NORMAL,也就是任务正常结束了
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
protected void setException(Throwable t) {
// 如果当前任务状态为NEW,则设置为COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
// 设置当前任务状态为EXCEPTIONAL,也就是任务非正常结束了
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
思考题,在什么时候多个线程会同时执行CAS将当前任务的状态从NEW转换为COMPLETING?
该方法的作用是,当任务执行完毕后,让其延迟固定时间后再次运行(fixed-delay任务)。
其中initialDelay表示提交任务后延迟多少时间开始执行command,delay表示当任务执行完毕后延长多少时间后再次运行command任务,unit是initialDelay和delay的时间单位。任务会一直重复运行直到任务中抛出了异常,别取消了,或者关闭了线程池。
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<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(-delay));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
// outerTask作用就是下一次定时执行的任务,在reExecutePeriodic方法中需要
sft.outerTask = t;
// 添加任务到延迟队列
delayedExecute(t);
return t;
}
需要看的是,这里传递给ScheduledFutureTask的period变量的值为-delay,delay<0说明该任务为可重复执行的任务。
将任务添加到延迟队列后,线程池线程会从队列里面获取任务,然后调用ScheduledFutureTask的run()执行。
ThreadPoolExecutor真正的线程工作类是内部类Worker,线程start启动的时候,会运行Worker的run方法,Worker的run()会循环从队列获取任务。
下面解析一下DelayedWorkQueue的take()方法
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 可中断的锁,优先响应中断
lock.lockInterruptibly();
try {
for (;;) {
// 获取队首元素
RunnableScheduledFuture<?> first = queue[0];
// 如果first为空,则当前线程进入条件队列,等待
if (first == null)
available.await();
else {
// 如果不为空,判断过期时间
long delay = first.getDelay(NANOSECONDS);
// 过期时间<=0,说明已经过期,那么直接出队列
if (delay <= 0)
return finishPoll(first);
// 在等待期间不能保留引用
first = null; // don't retain ref while waiting
// 如果leader线程不为空,则当前线程进入条件队列,等待
if (leader != null)
available.await();
else {
// 如果leader线程为空,则把当前线程赋值给leader
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 当前线程进入条件队列,等待delay时间,在等待时间内释放锁
// 等待时间结束后,会重新获取锁
available.awaitNanos(delay);
} finally {
// 重置leader线程为null,重新进入循环体,这时就会发现队首资源已经过期
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// leader为null,并且移除队首元素之后的新的队首元素不为空
if (leader == null && queue[0] != null)
// 激活条件队列里面的等待线程
available.signal();
// 释放资源
lock.unlock();
}
}
private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
int s = --size;
RunnableScheduledFuture<?> x = queue[s];
queue[s] = null;
if (s != 0)
// 调整堆的大小
siftDown(0, x);
// 移出队首元素
setIndex(f, -1);
return f;
}
取任务的流程如上,下面说一下添加任务的源码分析
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;
// 如果i=0,直接添加到队首
if (i == 0) {
queue[0] = e;
setIndex(e, 0);
} else {
// 否则调整堆的结构,按照时间调整
siftUp(i, e);
}
// 如果传入的元素正好是队首元素
if (queue[0] == e) {
// 设置leader线程为null
leader = null;
// 唤醒条件队列里等待的线程(一个)
available.signal();
}
} finally {
// 释放资源
lock.unlock();
}
// 添加任务成功,返回true
return true;
}
细心的读者可能发现take()和offer()里面都出现了leader变量,简单说一下leader变量。
leader变量是基于Leader-Follower模式的变体,用于尽量减少不必要的线程等待。当一个线程调用队列的take()变为leader线程后,他会调用条件变量available.awaitNanos(delay)等待delay时间,但是其他线程(follower线程)则会调用avaliable.await()进行无限等待。leader线程延迟时间过期后,获取任务,退出take(),并通过调用avaliable.signal()方法唤醒一个follower线程,被唤醒的follower线程被选举为新的leader线程。
言归正传,在讲一次性任务schedule()的时候,讲到了ScheduledFutureTask的run(),如果是周期性的任务会执行runAndReset(),不清楚的读者可以往上看看。
protected boolean runAndReset() {
// b
// 如果任务状态不是NEW则直接返回
// 如果当前状态是NEW,但是使用CAS设置当前任务的持有者为当前线程失败也返回
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return false;
// task执行成功的状态量
boolean ran = false;
int s = state;
try {
// 调用callable的call()执行具体任务
Callable<V> c = callable;
// 再次判断任务状态是否为NEW,是为了避免执行b代码块之后,其他线程修改了任务状态
// 比如取消了任务
if (c != null && s == NEW) {
try {
c.call(); // don't set result
ran = true;
} catch (Throwable ex) {
setException(ex);
}
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
// 任务正常执行完,并且任务状态为NEW,返回truee
return ran && s == NEW;
}
简单总结一下:
fixed-delay类型的任务,当添加一个任务到延迟队列后,会等待initialDelay时间,任务就会过期,过期的任务就会被队列移除,之后执行任务。执行完毕后,会重新设置任务的延迟时间,然后再把任务放入到延迟队列,循环执行。注意,如果一个任务在执行中出现了异常,那么这个任务就结束了,不影响其他任务的执行
该方法相对起始时间以固定频率调用指定的任务(fixed-rate任务)。当把任务提交到线程池并延迟initialDelay时间(时间单位为unit)后开始执行command。然后从initialDelay+period时间点开始再次执行,而后在initialDelay+n*period时间点再次执行,循环往复,直到抛出了异常或者调用了任务的cancel方法取消了任务,或者关闭了线程池。
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>0
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;
}
当前任务执行完毕后,调用setNextRunTime设置下次执行的时间,执行的是time+=period,不再是time=triggerTime(-p)。
简单论述了ScheduledThreadPoolExecutor的实现原理,内部使用DelayedWorkQueue来存放具体任务,任务类型用period的值来区分。
任务分为三种:第一种是一次性执行完毕的任务;第二种是fixed-delay,保证同一个任务在多次执行之间间隔固定时间。第三种是fixed-rate,抱枕任务按照固定的频率执行任务。
还有个问题,需要读者注意一下,由于DelayedQueue是无界队列,所以SecheduledThreadPoolExecutor只会建立corePoolSize个核心线程,所有的线程都是核心线程。