1、结构图
2、初识
ScheduledThreadPoolExecutor用来处理延时任务或定时任务。 流程如下:
2.1 定时任务分为四种 如下:
-
未来执行一次的任务,无返回值;
-
未来执行一次的任务,有返回值;
-
未来按固定频率重复执行的任务;
-
未来按固定延时重复执行的任务;
2.1.1 第一种与第二种差不多,如就是一个是有返回值,一个没有,以有返回值为例:
public class ScheduledThreadPoolExecutorTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10);
ScheduledFuture<String> schedule = executor.schedule(() -> {
return getUp("hello world");
}, 3000, TimeUnit.MILLISECONDS);
System.out.println("主线程无须等待。。。");
System.out.println("====获取定时线程池执行结果"+schedule.get());
}
public static String getUp(String s){
System.out.println("等待三秒后返回");
return s.toUpperCase();
}
/*public static void setUp(String s){
System.out.println("等待三秒后返回");
System.out.println(s.toUpperCase());
}*/
}
2.1.2 scheduleAtFixedRate()方法
按照固定频率重复执行的任务
示例代码如下:
public class ScheduledThreadPoolExecutorTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10)
executor.scheduleAtFixedRate(() -> {
System.out.println("hello world");
},1,2,TimeUnit.SECONDS);
}
}
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();
// 将普通任务装饰成ScheduledFutureTask
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
// 钩子方法,给子类用来替换装饰task,这里认为t==sft
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
// 延时执行
delayedExecute(t);
return t;
}
饰成另一个任务,再拿去执行,这里交给了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();
}
}
void ensurePrestart() {
// 创建工作线程
// 注意,这里没有传入firstTask参数,因为上面先把任务扔到队列中去了
// 另外,没用上maxPoolSize参数,所以最大线程数量在定时线程池中实际是没有用的
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
实际上,这里只是控制任务能不能被执行,真正执行任务的地方在任务的run()方法中。还记得上面的任务被装饰成了ScheduledFutureTask类的实例吗?所以,我们只要看ScheduledFutureTask的run()方法就可以了。
2.2 ScheduledFutureTask类的run()方法
/**
* 覆盖FutureTask版本,以便在定期重置/重新排队。
*/
public void run() {
// 是否重复执行
boolean periodic = isPeriodic();
// 线程池状态判断
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 一次性任务,直接调用父类的run()方法,这个父类实际上是FutureTask
else if (!periodic)
// 重复性任务,先调用父类的runAndReset()方法,这个父类也是FutureTask
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
// 设置下次执行的时间
setNextRunTime();
// 重复执行
reExecutePeriodic(outerTask);
}
}
可以看到,对于重复性任务,先调用FutureTask的runAndReset()方法,再设置下次执行的时间,最后再调用reExecutePeriodic()方法。
再来看看reExecutePeriodic()方法。
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
// 线程池状态检查
if (canRunInCurrentRunState(true)) {
// 再次把任务扔到任务队列中
super.getQueue().add(task);
// 再次检查线程池状态
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
// 保证工作线程足够
ensurePrestart();
}
}
到这里是不是豁然开朗了,原来定时线程池执行重复任务是在任务执行完毕后,又把任务扔回了任务队列中。重复性的问题解决了,那么,它是怎么控制任务在某个时刻执行的呢?这就轮到我们的延时队列登场了。
3、DelayedWorkQueue内部类
我们知道,线程池执行任务时需要从任务队列中拿任务,而普通的任务队列,如果里面有任务就直接拿出来了,但是延时队列不一样,它里面的任务,如果没有到时间也是拿不出来的,这也是前面分析中一上来就把任务扔进队列且创建Worker没有传入firstTask的原因。延时队列内部是使用“堆”这种数据结构来实现的。在执行定时任务的时候,每个任务的执行时间都不同,所以DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面(注意:这里的顺序并不是绝对的,堆中的排序只保证了子节点的下次执行时间要比父节点的下次执行时间要大,而叶子节点之间并不一定是顺序的)。