一、引言
在《并发编程的艺术》一书中对ScheduledThreadPoolExecutor的介绍还是基于jdk1.6版本,而在jdk1.7之后ScheduledThreadPoolExecutor发生了较大的变化,其使用的阻塞队列由之前的DelayQueue变成了DelayedWorkQueue。DelayedWorkQueue是DelayQueue(take进行delay判断)和PriorityQueue(堆结构)的结合。
ScheduledThreadPoolExecutor的功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。
ScheduledThreadPoolExecutor是一个用于执行定时任务或延时任务的线程池,提交到该线程池中的任务会等到执行时间到了才会被执行。与前面所讲的ThreadPoolExecutor不同,提交到ThreadPoolExecutor中的任务,只要ThreadPoolExecutor中有空闲线程任务会被从任务队列马上取出(或者未达到核心线程数),就会被马上执行,如果ThreadPoolExecutor中没有空闲线程,则任务会被暂存在任务队列。而提交到ScheduledThreadPoolExecutor中的任务,不管此时ScheduledThreadPoolExecutor有没有空闲线程,任务都会被放入到队列里去,等待任务执行时间到期时被线程从队列中取出并执行。
假如在看ScheduleThreadPoolExecutor的源码之前,我们凭自己思路设计一个定时任务线程池,需要怎么设计呢?
普通线程池的运行机制是从任务队列队首位置取出任务后,需要立马执行,但是定时任务线程池不一样,每个任务都有自己的定时执行时间,或者延时运行时间,任务队列,队首的任务也不是要立马执行。
因为需要定期执行,每个定时任务其实是重复执行,所以可以看做定时任务线程池一旦初始化建立,并启动后,队列里面的任务是固定的。每个任务都安排一个线程沉睡到指定执行的时间点,然后唤醒该线程并执行,貌似是可以的,但是假如这些任务的执行时间是没有重合,前后的时间间隔也比较大,明明一个线程就可以搞定,却新建了很多线程,占用内存,浪费资源,此时,阻塞任务队列也成为摆设,毫无意义。所以这种思路不可取。
既然每个任务都有一个线程蹲守watch方案不可取,那就只蹲守watch一个任务,那蹲守哪个任务呢?肯定是延时执行时间最临近当前时间的任务了,怎么知道哪个任务的延时执行时间是最临近的呢,必须有线程在添加新任务时候,给阻塞任务队列里面的任务,按照延时时间排排序,这就是涉及到适合延时任务队列的最优的排序的算法了。肯定还需要一个哨兵线程来蹲守即将到期的任务。
如果线程池里面的任务执行时间有重合,任务过期,线程数不够,发生拥挤又怎么办呢?该怎么优化?
JAVA的源码又是怎么做的呢?带着这些问题,去源码中寻找答案。
二、源码解析(JDK1.8)
2.1、构造方法
虽然ScheduledThreadPoolExecutor源码将构造方法放到后面展开,将它的专属任务类放到最前面,但是既然引言对其与父类ThreadPoolExcutor做了对比,这里还是先讲一下它设定的几个构造方法:
/**
* 默认线程工厂、拒绝策略
*/
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);
}
/**
* 自定义一个拒绝策略,如果线程池被shutDown后会用得着
*/
public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler) {
// 因为是无界队列,所以最大线程数是没有用的,如果核心线程数为0,那么就会只有一个工作线程
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), handler);
}
/**
* 几个构造方法生成的线程池都是最大线程数和无界队列,如果有程序一直往线程池提交延时任务,也会造成OOM的风险
* 所以使用该线程池一定有把握,延时任务不会瞬时超大批量的添加,当然实际使用中,该场景也比较少。
* 实在无法避免此风险,就自己另外定义一个子类,做扩展,并将队列和最大线程数设置成有界
* 并自定义合适拒绝策略
*/
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
ThreadPoolExcutor的父类AbstractExecutorService,里面的submit()方法:
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
2.1.1、可接受的任务task:FutureTask
虽然提交的都是Runnable或者Callable实现类,但其实都被包装成FutureTask,也就是RunnableFuture的实现类。FutureTask可以看做是Callable的包装类,内部有Callable 和保存其结果的 outcome ,同时它实现RunnableFuture也就是同时实现了 Runnable 和 Future ,有run() 和 get() 方法,run()调用的Callable的call(),将结果存在outcome,它构造方法可以接受Callable类型的参数,也可以接受Runnable类型的参数,但如果是Runnble,可不是直接用来实现Runnable接口用的,而是将Runnbale适配成Callable(Callable调用的是Runnbale的run(),FutureTask的run() 又是再调用call() 饶了一圈)。不管怎么样,它既然实现Runnable,也就是可以被装配到线程里异步运行。由此可见,它设计的思路,主要是融合Callable 和 Future,同时为了可以被提交到线程池,装配给线程执行,而实现Runable。说白了,它是整合了 Callable、Future、Runnable的产物。
它存在的意义还是实现可提交到线程异步执行,并能返回结果的任务类型(当然,如果调用get()获取返回结果,就是同步获取结果了),需要的话,可以读一下FutureTask的源码。既然ThreadPoolExcutor可接受的任务的定义这么重要,那么ScheduledThreadPoolExecutor可接受的延时任务,当然也重要,所以在ScheduledThreadPoolExecutor源码中,ScheduledFutureTask比较靠前。
2.2、ScheduledFutureTask
2.2.1、ScheduledFutureTask的UML图
2.2.2、ScheduledFutureTask源码
private class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V> {
/** 每个延时任务都有一个次序号,防止在在延时队列中排序的时候,
* 两个任务的延时时间一样,出现排序冲突,则通过次序号来决定
* 虽然任务是周期执行的,但是一旦任务被提交,次序号就定了,重复放入队列,并不会改变次序号
*/
private final long sequenceNumber;
/** 任务执行的时刻,在执行完后重复放入队列的时候,会被更新 */
private long time;
/**
* 循环执行的间隔时间
*/
private final long period;
/** The actual task to be re-enqueued by reExecutePeriodic */
RunnableScheduledFuture<V> outerTask = this;
/**
* 延时队列小顶堆中的下标
*/
int heapIndex;
/**
* 周期为0,非周期性任务,将runnable装配成callable再
*/
ScheduledFutureTask(Runnable r, V result, long ns) {
super(r, result);
this.time = ns;
this.period = 0;
this.sequenceNumber = sequencer.getAndIncrement();
}
/**
* Creates a periodic action with given nano time and period.
*/
ScheduledFutureTask(Runnable r, V result, long ns, long period) {
super(r, result);
this.time = ns;
this.period = period;
this.sequenceNumber = sequencer.getAndIncrement();
}
/**
* 非周期性任务callable
*/
ScheduledFutureTask(Callable<V> callable, long ns) {
super(callable);
this.time = ns;
this.period = 0;
this.sequenceNumber = sequencer.getAndIncrement();
}
/**
* 延时时间:RunnableScheduledFuture接口继承RunnableFuture、 ScheduleFuture, ScheduleFuture继承Delayed, Future
* Delaye(extends Comparable)接口有getDelay,延时队列的take() poll(long timeout, TimeUnit unit) 会用到
*/
public long getDelay(TimeUnit unit) {
return unit.convert(time - now(), NANOSECONDS);
}
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;
}
/**
* 源自RunnableScheduledFuture,是否周期性执行
*/
public boolean isPeriodic() {
return period != 0;
}
/**
* 下次执行时间
*/
private void setNextRunTime() {
long p = period;
if (p > 0)
time += p;
else
// 后面解析触发器
time = triggerTime(-p);
}
public boolean cancel(boolean mayInterruptIfRunning) {
boolean cancelled = super.cancel(mayInterruptIfRunning);
if (cancelled && removeOnCancel && heapIndex >= 0)
remove(this);
return cancelled;
}
/**
* 该任务task的run方法: 线程池 Worker中的线程start()执行,Worker -> worker.run() -> runWork()执行
* while (task != null || (task = getTask()) != null) {task.run}
*/
public void run() {
boolean periodic = isPeriodic();
// 是否还能运行,这个方法很重要,紧接着ScheduledThreadPoolExecutor内部源码解析会做详细解析
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
ScheduledFutureTask.super.run();
// cancle的任务就算被放进队列,这一步是不会执行的
else if (ScheduledFutureTask.super.runAndReset()) {
// 重新放入队列的时候,设置下次的执行的时间,time 在getDelay会用到,delay在
// 延时队列的take -> available.awaitNanos(delay);
setNextRunTime();
// 本次执行完,重新放入队列:可以理解为周期性反复执行
reExecutePeriodic(outerTask);
}
}
}