Java 线程池之 ScheduledThreadPoolExecutor 使用及源码分析

上一篇文章 Java 线程池之 ThreadPoolExecutor 源码分析 介绍了Java线程池的基本实现ThreadPoolExecutor是如何实现的,主要是通过将Runnable或Callable实现类,包装成FutureTask,然后在维护的workers线程池集合中某一线程中运行run()方法,启动线程。这里只是大致说下原理,具体可参考上篇文章中的分析。我们本篇是对线程池的续集,解说下带有定时功能的ScheduledThreadPoolExecutor是如何实现的。本篇我们换一种方式开始这个话题,分别从问题提出,问题分析,问题总结三方面进行分析,这或许将是本人以后的写作风格。

提出问题

在提出问题之前,我们先看下ScheduledThreadPoolExecutor是什么,怎么运行定时运行一个定时任务。以下一段代码是ScheduledThreadPoolExecutor的基本使用方法

public class ScheduledThreadPoolExecutorTest {

    public static void main(String[] args) {
        ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(3);

        // 延迟调度
        threadPoolExecutor.schedule(new RunnableTest("test1"), 1, TimeUnit.SECONDS);
        // 延迟后,周期性调度
        threadPoolExecutor.scheduleAtFixedRate(new RunnableTest("test2"), 0, 1, TimeUnit.SECONDS);
        // 延迟后,周期性调度
        threadPoolExecutor.scheduleWithFixedDelay(new RunnableTest("test3"), 0, 1, TimeUnit.SECONDS);
    }

    public static class RunnableTest implements Runnable {
        private String name;

        RunnableTest(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(name + " time: "  + new Date());
            try {
                // 特别注意此处睡眠时间
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果如下:

我们先来看下这段代码,main方法中注意是构造了一个核心线程数为3的ScheduledThreadPoolExecutor对象,然后调用提供的基本的三个调度方法。我们先根据这个结果分析下。

  • test1任务,在test2/test3后延迟1秒执行,之后后,没有再调度
  • test2任务,没有延迟执行,并且每次调用,间隔2s时间
  • test3任务,没有延迟执行,并且每次调用,间隔3s时间

上面结果,除了test1外,test2/test3两个任务,看起来都挺奇怪,明明设置的是间隔1s时间调度,为什么出现的结果是间隔3s。这里我们注意到RunnableTest的run方法中,执行了Thread.sleep(2000),睡眠了2s时间,这里我不进一步验证test2/test3为什么出现这样的情况,而直接给出结论,若想验证以下结论,可以通过修改Thread.sleep睡眠时间验证。

  • 从test1任务看出,执行schedule(Runnable command, long delay, TimeUnit unit)方法,可以延迟调度,不能周期性调度
  • 从test2任务看出,执行scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)方法,可以延迟调度,并且可以周期性调度,不过周期时间有两种:若当前任务在下一个周期时间未到之前完成,则按照周期间隔时间执行;若当前任务在下一个周期时间到达后未完成,则按照任务执行完成时间开始执行调度。即下一次任务调度完成时间,取下一次调度时间,和任务执行时长中,取较大值。
  • 从test3任务看出,执行scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)方法,可以延迟调度,并且可以周期性调度,下次任务执行时间为,当前任务执行完成后,延迟period时间开始执行。

 

以上内容为ScheduledThreadPoolExecutor的基本用法,各类文章中也都有介绍,那么我们可以带着一下问题,看下ScheduledThreadPoolExecutor如何实现定时调度。

  1. 如何实现一次性延迟调度
  2. 如何实现周期性固定频率调度
  3. 如何实现周期性固定延迟调度

问题分析

我们先看下ScheduledThreadPoolExecutor的集成依赖关系

从继承关系图,我们可以看出ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,所以他具有了ThreadPoolExecutor中的所有特征,已经调度方式,可以参考上一篇  Java 线程池之 ThreadPoolExecutor 源码分析 来了解ThreadPoolExecutor的实现,本文重点还是ScheduledThreadPoolExecutor。

我们首先从main方法中的ScheduledThreadPoolExecutor实例化开始,一步一步跟踪看下如何实现,首先看构造方法。

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}

这里可以看出,其实就是调用了父类的构造方法,主要需要注意的是DelayedWorkQueue这个队列,这里是实现延迟调度的关键之处,后续将会分析DelayedWorkQueue的实现。

我们继续从main方法中的schedule(Runnable command, long delay, TimeUnit unit)看

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    // decorateTask默认实现只是单纯的返回第二个参数task,可以通过继承的方式扩展
    RunnableScheduledFuture<?> t = decorateTask(command,
        new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit)));
    delayedExecute(t);
    return t;
}

这里decorateTask()方法只是返回了第二个参数,我们看下ScheduledFutureTask具体是什么

这里可以看出ScheduledFutureTask是FutureTask的一个具体实现,在 Java 线程池之 ThreadPoolExecutor 源码分析 中,已经对FutureTask中做了详细的讲解,这里不多赘述。根据上面代码,我们可以知道ScheduledThreadPoolExecutor是将Runnable实例,包装成了ScheduledFutureTask,通过以下构造方法。

ScheduledFutureTask(Runnable r, V result, long ns) {
    super(r, result);
    this.time = ns;
    this.period = 0;
    this.sequenceNumber = sequencer.getAndIncrement();
}
注意,这里的this.period设置了默认值0,表示没有周期调度。至于ns(任务运行时间),则通过triggerTime(delay, unit)方法开始执行获取,看下具体实现:
private long triggerTime(long delay, TimeUnit unit) {
    return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}

long triggerTime(long delay) {
    return now() + ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

我们继续回到schedule方法中,继续往下看,执行了delayedExecute(t);方法,具体实现如下:

private void delayedExecute(RunnableScheduledFuture<?> task) {
    // 如果shutdown,则拒绝任务
    if (isShutdown())
        reject(task);
    else {
        // 添加到等待队列中
        super.getQueue().add(task);
        // 重新判断是否shutdown,并且当前状态不能运行当前任务,则尝试将任务移除,并且取消任务
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        // 否则开始确保任务准备开始
        else
            ensurePrestart();
    }
}

代码注释中,已经解释了各个代码的作用,当任务可以运行时,将执行ensurePrestart()方法,

void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}

这里判断了当前线程数,是否达到核心线程数,若未达到,则添加新线程作为核心线程;若已达到,则添加新线程,不作为核心线程。至于addWorker是ThreadPoolExecutor中的核心方法,已在 Java 线程池之 ThreadPoolExecutor 源码分析 做了详细的解读,我们需要知道addWorker启动后,将会新建一个Worker线程,作为运行任务的线程,并且不断从任务队列中选取任务执行。Worker执行任务,是通过调用任务的run方法执行,我们看下ScheduledFutureTask的run方法是怎么实现的:

public void run() {
    boolean periodic = isPeriodic();
    // 判断当前状态能否运行
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // 判断是否为周期性任务,若不是,则直接运行父类FutureTask的run方法
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // 执行任务,执行完成后,若成功执行,则重置下次运行时间,并且将任务再次加入到队列中
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

这里首先判断是否能运行,若不能运行,则取消任务;然后判断是否为周期性任务,若不是,则运行父类FutureTask的run方法;若是周期性任务,则运行,并且重置任务状态为NEW;这里的FutureTask的run方法与runAndReset方法的区别在于,run方法就将任务状态更新,并且设置返回值,而runAndReset就重置任务状态,不设置返回值。

 

上面我们看到,任务如何添加到队列中去,下面我们将重点解读下,任务如何从队列中获取。

在ScheduledThreadPoolExecutor的构造方法中,我们看到,这里使用的队列是DelayedWorkQueue,DelayedWorkQueue是基于最小堆实现的,也就是说根元素是所有元素中最小的,每次获取元素只能从根元素获取。获取时,DelayedWorkQueue会判断根元素的延迟是否达到,若已达到,则开始运行,若未达到,则等待延迟时间后,再次尝试获取根元素,这样就能达到每次获取的都是最接近当前时间的任务。

关于DelayedWorkQueue的实现,我们将在下一篇文章中详细讲解。

问题总结

  1. ScheduledThreadPoolExecutor继承了ThreadPoolExecutor
  2. ScheduledThreadPoolExecutor执行任务时,先将任务添加到队列中,然后再取出,而不是执行运行
  3. ScheduledThreadPoolExecutor将任务包装成ScheduledFutureTask运行
  4. ScheduledThreadPoolExecutor使用DelayedWorkQueue队列,该队列作用时,将最近需要运行的任务,放在队列头部,以供提取运行
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值