Java定时调度机制 - ScheduledExecutorService

前言

通过上一篇文章【Java定时调度机制 - Timer】的分析,我们知道,Java的定时调度可以通过Timer&TimerTask来实现。由于其实现的方式为单线程,因此从JDK1.3发布之后就一直存在一些问题,大致如下:

  1. 多个任务之间会相互影响
  2. 多个任务的执行是串行的,性能较低

ScheduledExecutorService在设计之初就是为了解决Timer&TimerTask的这些问题。因为天生就是基于多线程机制,所以任务之间不会相互影响(只要线程数足够。当线程数不足时,有些任务会复用同一个线程)。

除此之外,因为其内部使用的延迟队列,本身就是基于等待/唤醒机制实现的,所以CPU并不会一直繁忙。同时,多线程带来的CPU资源复用也能极大地提升性能。

如何使用

基本作用

因为ScheduledExecutorService继承于ExecutorService,所以本身支持线程池的所有功能。额外还提供了4种方法,我们来看看其作用。

/**
 * 带延迟时间的调度,只执行一次
 * 调度之后可通过Future.get()阻塞直至任务执行完毕
 */
1. public ScheduledFuture<?> schedule(Runnable command,
                                      long delay, TimeUnit unit);

/**
 * 带延迟时间的调度,只执行一次
 * 调度之后可通过Future.get()阻塞直至任务执行完毕,并且可以获取执行结果
 */
2. public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                          long delay, TimeUnit unit);

/**
 * 带延迟时间的调度,循环执行,固定频率
 */
3. public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                 long initialDelay,
                                                 long period,
                                                 TimeUnit unit);

/**
 * 带延迟时间的调度,循环执行,固定延迟
 */
4. public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                    long initialDelay,
                                                    long delay,
                                                    TimeUnit unit);

1. schedule Runnable

该方法用于带延迟时间的调度,只执行一次。调度之后可通过Future.get()阻塞直至任务执行完毕。我们来看一个例子。

@Test public void test_schedule4Runnable() throws Exception {
    ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
    ScheduledFuture future = service.schedule(() -> {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task finish time: " + format(System.currentTimeMillis()));
    }, 1000, TimeUnit.MILLISECONDS);
    System.out.println("schedule finish time: " + format(System.currentTimeMillis()));

    System.out.println("Runnable future's result is: " + future.get() +
                       ", and time is: " + format(System.currentTimeMillis()));
}

上述代码达到的效果应该是这样的:延迟执行时间为1秒,任务执行3秒,任务只执行一次,同时通过Future.get()阻塞直至任务执行完毕。

我们运行看到的效果的确和我们猜想的一样,如下图所示。

执行结果

2. schedule Callable

在schedule Runnable的基础上,我们将Runnable改为Callable来看一下。

@Test public void test_schedule4Callable() throws Exception {
    ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
    ScheduledFuture<String> future = service.schedule(() -> {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task finish time: " + format(System.currentTimeMillis()));
        return "success";
    }, 1000, TimeUnit.MILLISECONDS);
    System.out.println("schedule finish time: " + format(System.currentTimeMillis()));

    System.out.println("Callable future's result is: " + future.get() +
            ", and time is: " + format(System.currentTimeMillis()));
}

运行看到的结果和Runnable基本相同,唯一的区别在于future.get()能拿到Callable返回的真实结果。

执行结果

3. scheduleAtFixedRate

该方法用于固定频率地对一个任务循环执行,我们通过一个例子来看看效果。

@Test public void test_scheduleAtFixedRate() {
    ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
    service.scheduleAtFixedRate(() -> {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task finish time: " + format(System.currentTimeMillis()));
    }, 1000L, 1000L, TimeUnit.MILLISECONDS);

    System.out.println("schedule finish time: " + format(System.currentTimeMillis()));
    while (true) {
    }
}

在这个例子中,任务初始延迟1秒,任务执行3秒,任务执行间隔为1秒。我们来看看执行结果:

执行结果

4. scheduleWithFixedDelay

该方法用于固定延迟地对一个任务循环执行,我们通过一个例子来看看效果。

@Test public void test_scheduleWithFixedDelay() {
    ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
    service.scheduleWithFixedDelay(() -> {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task finish time: " + format(System.currentTimeMillis()));
    }, 1000L, 1000L, TimeUnit.MILLISECONDS);

    System.out.println("schedule finish time: " + format(System.currentTimeMillis()));
    while (true) {
    }
}

在这个例子中,任务初始延迟1秒,任务执行3秒,任务执行间隔为1秒。我们来看看执行结果:

执行结果

5. scheduleAtFixedRate和scheduleWithFixedDelay的区别

既然这两个方法都是对任务循环执行,那么他们又有何区别呢?通过jdk文档我们找到了答案。

scheduleAtFixedRate - javadoc
scheduleWithFixedDelay - javadoc

直白地讲,scheduleAtFixedRate()为固定频率,scheduleWithFixedDelay()为固定延迟。固定频率是相对于任务执行的开始时间,而固定延迟是相对于任务执行的结束时间,这就是他们最根本的区别!

另外,从3和4的运行结果也能看出这些差异。

源码阅读初体验

一般源码的入口在于构造方法,我们来看看。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

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

在构造方法中我们看到以下信息:

  1. ScheduledThreadPoolExecutor构造方法最终调用的是ThreadPoolExecutor构造方法
  2. 阻塞队列使用的是DelayedWorkQueue

上述信息的第2点至关重要,但是限于篇幅,本文将不做深入分析。

接下来我们看看scheduleWithFixedDelay()方法,主要做了3件事情:

  1. 入参校验,包括空指针、数字范围
  2. Runnable包装成RunnableScheduledFuture
  3. 延迟执行RunnableScheduledFuture
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,
                                                 long delay,
                                                 TimeUnit unit) {
    // 1. 入参校验,包括空指针、数字范围
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0)
        throw new IllegalArgumentException();
    // 2. 将Runnable包装成`RunnableScheduledFuture`
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(-delay));
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    // 3. 延迟执行`RunnableScheduledFuture`
    delayedExecute(t);
    return t;
}

delayedExecute()这个方法从字面描述来看是延迟执行的意思,我们深入到这个方法里面去看看。

private void delayedExecute(RunnableScheduledFuture<?> task) {
    // 1. 线程池运行状态判断
    if (isShutdown())
        reject(task);
    else {
        // 2. 将任务添加到队列
        super.getQueue().add(task);
        // 3. 如果任务添加到队列之后,线程池状态变为非运行状态,
        // 需要将任务从队列移除,同时通过任务的`cancel()`方法来取消任务
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        // 4. 如果任务添加到队列之后,线程池状态是运行状态,需要提前启动线程
        else
            ensurePrestart();
    }
}

在线程池状态正常的情况下,最终会调用ensurePrestart()方法来完成线程的创建。主要逻辑有两个:

  1. 当前线程数未达到核心线程数,则创建核心线程
  2. 当前线程数已达到核心线程数,则创建非核心线程,不会将任务放到阻塞队列中,这一点是和普通线程池是不相同的
/**
 * Same as prestartCoreThread except arranges that at least one
 * thread is started even if corePoolSize is 0.
 */
void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    // 1. 当前线程数未达到核心线程数,则创建核心线程
    if (wc < corePoolSize)
        addWorker(null, true);
    // 2. 当前线程数已达到核心线程数,则创建非核心线程,
    // 2.1 不会将任务放到阻塞队列中,这一点是和普通线程池是不相同的
    else if (wc == 0)
        addWorker(null, false);
}

至此,除了DelayedWorkQueue延迟队列的源码还未分析,其他的我们都分析完了。

总结

首先,我们了解了ScheduledExecutorService的基本作用,然后在此基础上写了一些demo来做验证,得到的结果和基本作用是完全相同的。

然后,我们对其内部的实现原理和源代码做了初步的分析,知道了其和普通线程池是不同的地方在于:阻塞队列创建线程的方式

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容

  • 原文链接 译者:靖靖 并发 进程和线程 在并发编程当中,有两个基本的执行单元:进程和线程。在java中,我们大部分...
    4b4f3ceb6f71阅读 807评论 4 16
  • https://blog.csdn.net/defonds/article/details/44021605/ 译...
    huangxiongbiao阅读 1,251评论 0 11
  • 是的 我依然爱你, 这种爱 不会因世事变迁而消弭。 只是 慢慢地,慢慢地 沉到心底, 酿成一段回忆。 爱过 就很美...
    勿须书生阅读 139评论 0 2
  • (一) “老三,醒醒。天黑了,你该上班了。” 老三撑开惺忪的眼皮,早上喝大了,脑袋到现在还一阵阵发疼。还没来得及坐...
    馒头不想吃面包阅读 227评论 0 3
  • 男二一直是偶像剧言情剧里神奇的存在。男二就是往往能斩获万千观众的少女心,却独独得不到女主的爱的那个人。 男二对女主...
    小淘米_TTMIX阅读 305评论 0 0