Java定时任务之ScheduledThreadPoolExecutor

345 阅读3分钟

《# Java定时任务之Timer》一文中,我们研究了Timer的内部原理。现在,来看看比Timer功能更强大、更灵活的ScheduledThreadPoolExecutor

在JDK 1.5中,新增了ScheduledThreadPoolExecutor类,继承自ThreadPoolExecutor。主要用来执行延迟任务,或者定期执行任务。

1 ScheduledThreadPoolExecutor

构造器方法如下。DelayedWorkQueue是一个无界队列,因此maximumPoolSize失去意义。 image.png 使用以下提交任务,会创建一个ScheduledFutureTask对象。 image.png 通过sequencer属性,给每个任务生成一个序号(当任务) image.png

2 ScheduledFutureTask

注意,它实现了Runnable接口、Comparable接口。 image.png

2.1 属性和构造器

主要包含5个成员变量:

  • long time,将要被执行的具体时间。
  • long sequenceNumber,任务被添加到ScheduledThreadPoolExecutor时序号。
  • long period,间隔周期,period=0即仅执行一次的delay任务
  • outerTask,对自身引用,在周期任务时会将它再次添加到队列中。
  • callable,任务本身,这个属性在父类FutureTask中。 image.png 构造方法如下。 image.png super(r, result);指向了FutureTask。 image.png

2.2 周期任务实现

任务延迟执行通过DelayQueue实现。周期执行是如何实现的?

其实,工作线程执行任务时,不是直接调用你提交任务的run(),而是执行ScheduledFutureTask.run()

  • 如果是仅执行一次的delay任务,则调用任务并结束;
  • 如果是周期任务,除了调用任务本身,还要重置time为下次时间,再次将任务放回队列中。 image.png image.png image.png 周期任务的执行流程图如下: image.png

3 DelayedWorkQueue

DelayedWorkQueue实现了BlockingQueue,内部是一个数组,初始容量为16,使用堆结构,提供了siftUpsiftDown方法,实现了优先级image.png image.png

3.1 阻塞实现

对于BlockingQueue,在队列为空时调用take()将阻塞,在队列已满时调用offer()也阻塞。 而DelayedWorkQueue有所不同:

  • 当内部数组已满时调用offer(),不会阻塞;将触发扩容,因此尺寸相当于无界的。 image.png
  • 队列为空时调用take()将阻塞;但不为空时,如果首个任务没有到期,也会计时await阻塞。 image.png

3.2 队列中元素排序

ScheduledFutureTask实现了Comparable接口。排序规则如下:

  • time小的排在前面,
  • time相同时比较sequenceNumber,sequenceNumber更小的排前面;
  • sequenceNumber在任务被提交时生成,值越小说明被提交越早。 image.png 因此,DelayedWorkQueue中第一个元素,始终是最早到期的那个任务。

4 使用ScheduledThreadPoolExecutor

4.1 与Timer比较

  1. 由于使用了线程池,当执行任务发生异常导致线程退出,线程池会补充工作线程,或使用其他线程来继续执行队列中任务。
  2. 也是由于使用线程池,多个类型的任务间可以并发执行,而非串行。
  3. 都是基于优先级队列,来实现任务按到期时间排序;
  4. 对于一个周期任务,多次执行之间都是串行的,不会并发;

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);
  }
}

image.png

不同任务使用不同线程池

当不同任务提交到一个线程池时,且任务数大于线程池线程数时,执行周期会相互影响,导致延迟。如下:

  • 线程数为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);

image.png