5、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor可另外调度在给定延迟之后运行的命令,或定期执行。 ScheduledThreadPoolExecutor的功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。
ScheduledThreadPoolExecutor提交、执行任务的方法可分为延迟、周期两种。因前文对ThreadPoolExecutor已经做了较为详细的介绍,本篇
在正式开始分析ScheduledThreadPoolExecutor执行过程之前,先来看一下其构造函数:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}
特别介绍一下其构造函数的目的是为了说明ScheduledThreadPoolExecutor使用了自己内部的任务队列DelayedWorkQueue,该类实现了BlockingQueue接口,其作用我们下文分析。
5.1、 延迟执行
// 示例
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(4);
executor.schedule(new Runnable() {
@Override
public void run() {
System.out.println("延迟10秒执行");
}
}, 10, TimeUnit.SECONDS);
通过上面的配置,可以实现任务的延迟执行。
// 延迟执行任务
// command:任务;delay:延迟时间;unit:延迟时间单位
public ScheduledFuture> schedule(Runnable command, long delay, TimeUnit unit) {
// 参数校验
if (command == null || unit == null)
throw new NullPointerException();
// 将任务包装成RunnableScheduledFuture对象
RunnableScheduledFuture> t = decorateTask(command,
new ScheduledFutureTask(command, null,triggerTime(delay, unit)));
// 延迟执行
delayedExecute(t);
return t;
}
// 延迟执行
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();
}
}
任务加入阻塞队列后,线程池的状态可能发生了变化,所以要进行二次校验。这里涉及到一个知识点,先来看ScheduledThreadPoolExecutor中的两个变量:
// 线程池关闭后(SHUTDOWN)是否继续执行周期性任务。默认false
private volatile boolean continueExistingPeriodicTasksAfterShutdown;
// 线程池关闭后(SHUTDOWN)是否继续执行延迟性任务。默认true
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;
当线程处于非运行状态时,根据任务性质(延迟、周期)并结合这两个变量值以确定任务是否继续执行。
// 预启动
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
5.2、获取任务
因为延迟、周期任务需要延期执行,所以addWorker()方法并没有传入具体的任务,而是从任务队列中获取。这里先要了解一下Leader-Follower模式。
[图片上传失败...(image-faba54-1605494023030)]
上图就是L/F多线程模型的状态变迁图,共6个关键点:
- 线程有3种状态:领导leading,处理processing,追随following。
- 假设共N个线程,其中只有1个leading线程(等待任务),x个processing线程(处理),余下有N-1-x个following线程(空闲)。
- 有一把锁,谁抢到就是leading。
- 事件/任务来到时,leading线程会对其进行处理,从而转化为processing状态,处理完成之后,又转变为following。
- 丢失leading后,following会尝试抢锁,抢到则变为leading,否则保持following。
- following不干事,就是抢锁,力图成为leading。
以上Leader-Follower内容摘抄自W3CSchool
了解了Leader-Follower线程模型后,再来看take()方法就事半功倍。take()方法的大体思想可以看做是Leader-Follower线程模型的一种变体。使用这种模式可以有效的减少不必要的等待时间。当一个线程成为leader时,它只等待下一个延迟过去,而其他线程则无限期地等待。在从take()或poll()返回之前,leader线程必须给其他线程发送信号,除非在此期间有其他线程成为leader。每当队列的头部被一个过期时间较早的任务替换时,leader字段就会被重置为null,一些等待的线程(不一定是当前的leader)就会被发送信号。
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
// 若已经有线程成为leader,则其他线程等待leader线程执行完毕
if (leader != null)
available.await();
else {
// 无线程成为leader,则将当前线程置为leader,并等待延迟时间过去
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
// 释放leader
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// leader已释放、且队列中还有待执行任务,通知其他线程
// 其他线程将被唤醒,并争抢leader
if (leader == null && queue[0] != null)
available.signal();
// 释放锁
lock.unlock();
}
}
take()方法虽然思想简单,对于线程基础薄弱的同学,理解代码执行过程较为困难。take方法虽然使用ReentrantLock对方法加锁,但是在leader线程等待延迟执行的时候,通过调用Condition的awaitNanos()方法释放同步状态,使得其它线程依然可以获取锁。而其它线程进入take()方法后,发现leader线程不为空,则调用Condition的await()方法无限期等待,直至leader线程发起通知。
5.3、执行任务
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);
}
}
前文已经介绍过,ScheduledThreadPoolExecutor会对任务做一层包装,包装底层使用的是FutureTask。FutureTask会在下一小节介绍,这里我们只需要知道周期任务是如何重复执行即可。