在《# Java定时任务之Timer》一文中,我们研究了Timer的内部原理。现在,来看看比Timer功能更强大、更灵活的ScheduledThreadPoolExecutor。
在JDK 1.5中,新增了ScheduledThreadPoolExecutor类,继承自ThreadPoolExecutor。主要用来执行延迟任务,或者定期执行任务。
1 ScheduledThreadPoolExecutor
构造器方法如下。DelayedWorkQueue是一个无界队列,因此maximumPoolSize失去意义。
使用以下提交任务,会创建一个ScheduledFutureTask对象。
通过sequencer属性,给每个任务生成一个序号(当任务)
2 ScheduledFutureTask
注意,它实现了Runnable接口、Comparable接口。
2.1 属性和构造器
主要包含5个成员变量:
- long time,将要被执行的具体时间。
- long sequenceNumber,任务被添加到ScheduledThreadPoolExecutor时序号。
- long period,间隔周期,period=0即仅执行一次的delay任务
- outerTask,对自身引用,在周期任务时会将它再次添加到队列中。
- callable,任务本身,这个属性在父类FutureTask中。
构造方法如下。
super(r, result);指向了FutureTask。
2.2 周期任务实现
任务延迟执行通过DelayQueue实现。周期执行是如何实现的?
其实,工作线程执行任务时,不是直接调用你提交任务的run(),而是执行ScheduledFutureTask.run():
- 如果是仅执行一次的delay任务,则调用任务并结束;
- 如果是周期任务,除了调用任务本身,还要重置time为下次时间,再次将任务放回队列中。
周期任务的执行流程图如下:
3 DelayedWorkQueue
DelayedWorkQueue实现了BlockingQueue,内部是一个数组,初始容量为16,使用堆结构,提供了siftUp、siftDown方法,实现了优先级。
3.1 阻塞实现
对于BlockingQueue,在队列为空时调用take()将阻塞,在队列已满时调用offer()也阻塞。
而DelayedWorkQueue有所不同:
- 当内部数组已满时调用
offer(),不会阻塞;将触发扩容,因此尺寸相当于无界的。 - 队列为空时调用
take()将阻塞;但不为空时,如果首个任务没有到期,也会计时await阻塞。
3.2 队列中元素排序
ScheduledFutureTask实现了Comparable接口。排序规则如下:
- time小的排在前面,
- time相同时比较sequenceNumber,sequenceNumber更小的排前面;
- sequenceNumber在任务被提交时生成,值越小说明被提交越早。
因此,DelayedWorkQueue中第一个元素,始终是最早到期的那个任务。
4 使用ScheduledThreadPoolExecutor
4.1 与Timer比较
- 由于使用了线程池,当执行任务发生异常导致线程退出,线程池会补充工作线程,或使用其他线程来继续执行队列中任务。
- 也是由于使用线程池,多个类型的任务间可以并发执行,而非串行。
- 都是基于优先级队列,来实现任务按到期时间排序;
- 对于一个周期任务,多次执行之间都是串行的,不会并发;
4.2 使用注意事项
合理设置执行周期
下面代码中,任务执行时间大于period,将不会按period来触发。虽然线程池中有多个线程。
注意,周期任务是在本次执行后,修改time并再放回队列的。
@Slf4j(topic = "c.TestTimer")
public class TestScheduledExecutorService {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
try {
log.debug("running...");
// 执行耗时大于period
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, 1, 2, TimeUnit.SECONDS);
}
}
不同任务使用不同线程池
当不同任务提交到一个线程池时,且任务数大于线程池线程数时,执行周期会相互影响,导致延迟。如下:
- 线程数为2,任务数为3;
- task1、task2都是耗时5秒,周期10秒;task3耗时2秒,周期4秒。
- task3原本延迟1秒启动,结果延迟了5秒,周期变得不规律
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
try {
log.debug("running task1");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, 1, 10, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(() -> {
try {
log.debug("running task2");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, 1, 10, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(() -> {
try {
log.debug("running task3");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, 1, 4, TimeUnit.SECONDS);