一文搞懂Java ScheduledExecutorService的使用

目录
  • 一、创建ScheduledExecutorService对象
  • 二、ScheduledExecutorService方法
  • 三、固定速率和固定延时的区别
    • 1. 固定速率
    • 2. 固定延时
  • 四、调度多个任务
  • 五、其他要点

JUC包(java.util.concurrent)中提供了对定时任务的支持,即ScheduledExecutorService接口。

本文对ScheduledExecutorService的介绍,将基于Timer类使用介绍进行,因此请先阅读Timer类使用介绍文章。

此处为语雀内容卡片,点击链接查看

一、创建ScheduledExecutorService对象

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);

二、ScheduledExecutorService方法

ScheduledExecutorService实现了ExecutorService接口,ExecutorService接口中的方法事实上属于线程池相关的一般方法,不在本文讨论。

ScheduledExecutorService本身提供了以下4个方法:

  • ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit):延迟delay单位时间后,执行一次任务
  • <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit):延迟delay单位时间后,执行一次任务
  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):延迟initialDelay单位时间后,执行一次任务,之后每隔period单位时间执行一次任务(固定速率)
  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):延迟initialDelay单位时间后,执行一次任务,之后每隔period单位时间执行一次任务(固定延时)

ScheduledExecutorService和Timer进行对比,两者所提供的方法是类似的,区别在于Timer有提供指定时间点执行任务,而ScheduledExecutorService没有提供。

Timer提供的方法返回值均为void,而ScheduledExecutorService的方法返回值均为ScheduledFuture(继承于Future接口)。

三、固定速率和固定延时的区别

和Timer一样,我们用示例来展示ScheduledExecutorService固定速率和固定延时的区别,并与Timer进行对比。

1. 固定速率

示例:

System.out.println("启动于:" + DateUtil.formatNow());
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
executorService.scheduleAtFixedRate(
        new Runnable() {
            int i = 1;
            @Override
            public void run() {
                System.out.print(i + " " + DateUtil.formatNow() + " 开始执行, ");
                if(i == 3) {
                    ThreadUtil.sleep(11 * 1000);
                }
                System.out.println(DateUtil.formatNow() + " 结束");
                i ++;
            }
        },
5, 2, TimeUnit.SECONDS);

输出:

启动于:2022-10-31 17:15:44
1 2022-10-31 17:15:49 开始执行, 2022-10-31 17:15:49 结束
2 2022-10-31 17:15:51 开始执行, 2022-10-31 17:15:51 结束
3 2022-10-31 17:15:53 开始执行, 2022-10-31 17:16:04 结束 *
4 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
5 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
6 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
7 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
8 2022-10-31 17:16:04 开始执行, 2022-10-31 17:16:04 结束 *
9 2022-10-31 17:16:05 开始执行, 2022-10-31 17:16:05 结束
10 2022-10-31 17:16:07 开始执行, 2022-10-31 17:16:07 结束
11 2022-10-31 17:16:09 开始执行, 2022-10-31 17:16:09 结束

没有11秒耗时的情况下,正常应该是输出:

启动于:2022-10-31 17:15:44
1 2022-10-31 17:15:49 开始执行, 2022-10-31 17:15:49 结束
2 2022-10-31 17:15:51 开始执行, 2022-10-31 17:15:51 结束
3 2022-10-31 17:15:53 开始执行, 2022-10-31 17:15:53 结束
4 2022-10-31 17:15:55 开始执行, 2022-10-31 17:15:55 结束
5 2022-10-31 17:15:57 开始执行, 2022-10-31 17:15:57 结束
6 2022-10-31 17:15:59 开始执行, 2022-10-31 17:15:59 结束
7 2022-10-31 17:16:01 开始执行, 2022-10-31 17:16:01 结束
8 2022-10-31 17:16:03 开始执行, 2022-10-31 17:16:03 结束
9 2022-10-31 17:16:05 开始执行, 2022-10-31 17:16:05 结束
10 2022-10-31 17:16:07 开始执行, 2022-10-31 17:16:07 结束
11 2022-10-31 17:16:09 开始执行, 2022-10-31 17:16:09 结束

从测试结果中可以看出,当有一次任务执行耗时过长,超出了设定的period时间单位,将会影响后续5次任务准时执行,当耗时任务完成后,ScheduledExecutorService将会立即将延误的5次任务一起补上,并保障后续的任务按预期的时间点执行。

这与ScheduledExecutorService固定速率的效果与Timer是完全一样的,读者可直接参考Timer的固定速率介绍。

2. 固定延时

示例:

System.out.println("启动于:" + DateUtil.formatNow());
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
executorService.scheduleWithFixedDelay(
        new Runnable() {
            int i = 1;
            @Override
            public void run() {
                System.out.print(i + " " + DateUtil.formatNow() + " 开始执行, ");
                if(i == 3) {
                    ThreadUtil.sleep(11 * 1000);
                }
                System.out.println(DateUtil.formatNow() + " 结束");
                i ++;
            }
        },
5, 2, TimeUnit.SECONDS);

输出:

1 2022-10-31 17:16:41 开始执行, 2022-10-31 17:16:41 结束
2 2022-10-31 17:16:43 开始执行, 2022-10-31 17:16:43 结束
3 2022-10-31 17:16:45 开始执行, 2022-10-31 17:16:56 结束 *
4 2022-10-31 17:16:58 开始执行, 2022-10-31 17:16:58 结束
5 2022-10-31 17:17:00 开始执行, 2022-10-31 17:17:00 结束
6 2022-10-31 17:17:02 开始执行, 2022-10-31 17:17:02 结束
7 2022-10-31 17:17:04 开始执行, 2022-10-31 17:17:04 结束
8 2022-10-31 17:17:06 开始执行, 2022-10-31 17:17:06 结束
9 2022-10-31 17:17:08 开始执行, 2022-10-31 17:17:08 结束

没有11秒耗时的情况下,正常应该是输出:

1 2022-10-31 17:16:41 开始执行, 2022-10-31 17:16:41 结束
2 2022-10-31 17:16:43 开始执行, 2022-10-31 17:16:43 结束
3 2022-10-31 17:16:45 开始执行, 2022-10-31 17:16:45 结束
4 2022-10-31 17:16:47 开始执行, 2022-10-31 17:16:47 结束
5 2022-10-31 17:16:49 开始执行, 2022-10-31 17:16:49 结束
6 2022-10-31 17:16:51 开始执行, 2022-10-31 17:16:51 结束
7 2022-10-31 17:16:53 开始执行, 2022-10-31 17:16:53 结束
8 2022-10-31 17:16:55 开始执行, 2022-10-31 17:16:55 结束
9 2022-10-31 17:16:57 开始执行, 2022-10-31 17:16:57 结束

固定延时是当任务执行耗时过长,超出设定的delay时间单位,后续的任务将会被顺延推迟,这个设计是与Timer一样的,但与Timer却有一点小区别。

在Timer类使用介绍中,曾提到Timer类固定延时下与我想象的不太一致,Timer在第3次任务执行完成后会立即执行第4次任务,接着才是间隔2秒执行第5次任务。

而ScheduledExecutorService则与我的想象完全一致,当第3次任务执行完成后,会间隔2秒再执行第4次任务。

所以固定延时下,Timer和ScheduledExecutorService的实现是有一点区别的。

四、调度多个任务

在Timer中,一个TimerTask对象是一个任务。

而在ScheduledExecutorService中,则一个Runnable对象一个任务。

第三节介绍的是固定速率和固定延时是如何影响一个可重复执行任务(一个Runnable对象)的多次执行的。

而本节介绍的是ScheduledExecutorService如何同时调度多个可重复执行任务的。

与Timer内部仅1个线程不同,ScheduledExecutorService内部采用的是线程池,是支持自己设定线程数的。

那么理论上来说,如果要加入2个任务,ScheduledExecutorService设定线程数为2,就不会出现相互影响的情况。

我们来验证一下。

定义任务,当执行第3次时将会休眠11秒:

class Task implements Runnable {

    private int i = 1;

    private String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(i + " " + name + ":" + DateUtil.formatNow() + " 开始执行");
        if(i == 3) {
            ThreadUtil.sleep(11 * 1000);
        }
        System.out.println(i + " " + name + ":" + DateUtil.formatNow() + " 执行结束");
        i ++;
    }
}

使用ScheduledExecutorService进行调度:

System.out.println("启动于:" + DateUtil.formatNow());
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);

Task task1 = new Task("task1");
Task task2 = new Task("task2");

executorService.scheduleWithFixedDelay(task1, 5, 2, TimeUnit.SECONDS);
executorService.scheduleWithFixedDelay(task2, 5, 2, TimeUnit.SECONDS);

由于控制台输出时,task1和task2的日志会混在一起,不容易阅读,我这边将task1和task2的日志分开。

task1日志:

启动于:2022-10-31 17:49:51
1 task1:2022-10-31 17:49:56 开始执行
1 task1:2022-10-31 17:49:56 执行结束
2 task1:2022-10-31 17:49:58 开始执行
2 task1:2022-10-31 17:49:58 执行结束
3 task1:2022-10-31 17:50:00 开始执行
3 task1:2022-10-31 17:50:11 执行结束
4 task1:2022-10-31 17:50:13 开始执行
4 task1:2022-10-31 17:50:13 执行结束
5 task1:2022-10-31 17:50:15 开始执行
5 task1:2022-10-31 17:50:15 执行结束

task2日志:

启动于:2022-10-31 17:49:51
1 task2:2022-10-31 17:49:56 开始执行
1 task2:2022-10-31 17:49:56 执行结束
2 task2:2022-10-31 17:49:58 开始执行
2 task2:2022-10-31 17:49:58 执行结束
3 task2:2022-10-31 17:50:00 开始执行
3 task2:2022-10-31 17:50:11 执行结束
4 task2:2022-10-31 17:50:13 开始执行
4 task2:2022-10-31 17:50:13 执行结束
5 task2:2022-10-31 17:50:15 开始执行

经过测试可以确定,当加入的任务数不超过线程池线程数时,即使任务存在耗时也不会相互影响,而仅是影响自身任务下一次执行的时间点。

那如果加入任务数超出了线程数呢?

我们测试一下加入3个任务,线程数仍然为2.

Task task1 = new Task("task1");
Task task2 = new Task("task2");
Task task3 = new Task("task3");

executorService.scheduleWithFixedDelay(task1, 5, 2, TimeUnit.SECONDS);
executorService.scheduleWithFixedDelay(task2, 5, 2, TimeUnit.SECONDS);
executorService.scheduleWithFixedDelay(task3, 5, 2, TimeUnit.SECONDS);

将三个任务的日志分开展示。

task1:

启动于:2022-10-31 17:53:22
1 task1:2022-10-31 17:53:27 开始执行
1 task1:2022-10-31 17:53:27 执行结束
2 task1:2022-10-31 17:53:29 开始执行
2 task1:2022-10-31 17:53:29 执行结束
3 task1:2022-10-31 17:53:31 开始执行
3 task1:2022-10-31 17:53:42 执行结束
4 task1:2022-10-31 17:53:44 开始执行
4 task1:2022-10-31 17:53:44 执行结束
5 task1:2022-10-31 17:53:46 开始执行
5 task1:2022-10-31 17:53:46 执行结束
6 task1:2022-10-31 17:53:48 开始执行
6 task1:2022-10-31 17:53:48 执行结束
7 task1:2022-10-31 17:53:50 开始执行
7 task1:2022-10-31 17:53:50 执行结束
8 task1:2022-10-31 17:53:52 开始执行
8 task1:2022-10-31 17:53:52 执行结束
9 task1:2022-10-31 17:53:54 开始执行
9 task1:2022-10-31 17:53:54 执行结束
10 task1:2022-10-31 17:53:56 开始执行
10 task1:2022-10-31 17:53:56 执行结束

task2:

启动于:2022-10-31 17:53:22
1 task2:2022-10-31 17:53:27 开始执行
1 task2:2022-10-31 17:53:27 执行结束
2 task2:2022-10-31 17:53:29 开始执行
2 task2:2022-10-31 17:53:29 执行结束
3 task2:2022-10-31 17:53:31 开始执行
3 task2:2022-10-31 17:53:42 执行结束
4 task2:2022-10-31 17:53:44 开始执行
4 task2:2022-10-31 17:53:44 执行结束
5 task2:2022-10-31 17:53:46 开始执行
5 task2:2022-10-31 17:53:46 执行结束
6 task2:2022-10-31 17:53:48 开始执行
6 task2:2022-10-31 17:53:48 执行结束
7 task2:2022-10-31 17:53:50 开始执行
7 task2:2022-10-31 17:53:50 执行结束
8 task2:2022-10-31 17:53:52 开始执行
8 task2:2022-10-31 17:53:52 执行结束
9 task2:2022-10-31 17:53:54 开始执行
9 task2:2022-10-31 17:53:54 执行结束
10 task2:2022-10-31 17:53:56 开始执行
10 task2:2022-10-31 17:53:56 执行结束

task3:

启动于:2022-10-31 17:53:22
1 task3:2022-10-31 17:53:27 开始执行
1 task3:2022-10-31 17:53:27 执行结束
2 task3:2022-10-31 17:53:29 开始执行
2 task3:2022-10-31 17:53:29 执行结束
3 task3:2022-10-31 17:53:42 开始执行
3 task3:2022-10-31 17:53:53 执行结束
4 task3:2022-10-31 17:53:55 开始执行
4 task3:2022-10-31 17:53:55 执行结束
5 task3:2022-10-31 17:53:57 开始执行
5 task3:2022-10-31 17:53:57 执行结束

从以上日志可以看出,task1和task2执行是正常的,但是task3从第3次执行开始出现错误。

task3第三次时间点正确时间应该是17:53:31,而实际上被推迟到了17:53:42才开始。

从这点我们可以推测出,当时2个线程都在执行task1、task2的耗时11秒的第3次任务,导致task3被推迟。

因此,我们在使用ScheduledExecutorService调度多个任务时,应注意尽可能缩短任务的处理耗时,以及避免任务数超出线程数。

五、其他要点

任务执行过程中抛出异常会发生什么情况?

Timer内部是单个线程处理所有任务,当抛出异常时,Timer线程将终止运行;

ScheduledExecutorService内部是一个线程池,当抛出异常时,此任务所在线程将会终止运行被回收,该任务后续无法再触发执行,其他线程不受影响,因此编写任务执行代码要注意捕获异常。

以上就是一文搞懂Java ScheduledExecutorService的使用的详细内容,更多关于Java ScheduledExecutorService的资料请关注我们其它相关文章!

(0)

相关推荐

  • 基于ScheduledExecutorService的两种方法(详解)

    开发中,往往遇到另起线程执行其他代码的情况,用java定时任务接口ScheduledExecutorService来实现. ScheduledExecutorService是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响. 注意,只有当调度任务来的时候,ScheduledExecutorService才会真正启动一个线程,其余时间ScheduledExecutorService都是处于轮询任务的状态. 1.scheduleAtFix

  • java多线程开发ScheduledExecutorService简化方式

    目录 前言 java多线程的应用场景 应用场景一: 应用场景二: ScheduledExecutorService方法简介 实例 实例结果 前言 java开发,多多少少会接触到多线程的应用开发场景,博主之前做多线程的时候就两种方式,一个是继承Thread一个是实现Runnable接口,不过用的多的还是实现Runnable接口,因为实现Runnable接口可以做多线程资源共享!而java.util.concurrent.ScheduledExecutorService接口将大大简化多线程应用的开发

  • ScheduledExecutorService任务定时代码示例

    本文主要分享了一则关于ScheduledExecutorService任务定时的实例代码,具体如下: 示例代码 package com.effective.common.concurrent.execute; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurre

  • Java ScheduledExecutorService定时任务案例讲解

    一.ScheduledExecutorService 设计思想 ScheduledExecutorService,是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响. 需要注意,只有当调度任务来的时候,ScheduledExecutorService才会真正启动一个线程,其余时间ScheduledExecutorService都是出于轮询任务的状态. 1.线程任务 class MyScheduledExecutor implement

  • 一文搞懂Java ScheduledExecutorService的使用

    目录 一.创建ScheduledExecutorService对象 二.ScheduledExecutorService方法 三.固定速率和固定延时的区别 1. 固定速率 2. 固定延时 四.调度多个任务 五.其他要点 JUC包(java.util.concurrent)中提供了对定时任务的支持,即ScheduledExecutorService接口. 本文对ScheduledExecutorService的介绍,将基于Timer类使用介绍进行,因此请先阅读Timer类使用介绍文章. 此处为语雀

  • 一文搞懂Java创建线程的五种方法

    目录 题目描述 解题思路 代码详解 第一种 继承Thread类创建线程 第二种:实现Runnable接口创建线程 第三种:实现Callable接口,通过FutureTask包装器来创建Thread线程 第四种:使用ExecutorService.Callable(或者Runnable).Future实现返回结果的线程 第五种:使用ComletetableFuture类创建异步线程,且是据有返回结果的线程 题目描述 Java创建线程的几种方式 Java使用Thread类代表线程,所有线程对象都必须

  • 一文搞懂JAVA 修饰符

    Java语言提供了很多修饰符,主要分为以下两类: 访问修饰符 非访问修饰符 修饰符用来定义类.方法或者变量,通常放在语句的最前端.我们通过下面的例子来说明: public class ClassName { // ... } private boolean myFlag; static final double weeks = 9.5; protected static final int BOXWIDTH = 42; public static void main(String[] argum

  • 一文搞懂JAVA 枚举(enum)

    Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等. Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割. 例如定义一个颜色的枚举类. enum Color { RED, GREEN, BLUE; } 以上枚举类 Color 颜色常量有 RED, GREEN, BLUE,分别表示红色,绿色,蓝色. 使用实例: enum Color { RED, GREEN, BLUE; } public c

  • 一文搞懂Java中的反射机制

    前一段时间一直忙,所以没什么时间写博客,拖了这么久,也该更新更新了.最近看到各种知识付费的推出,感觉是好事,也是坏事,好事是对知识沉淀的认可与推动,坏事是感觉很多人忙于把自己的知识变现,相对的在沉淀上做的实际还不够,我对此暂时还没有什么想法,总觉得,慢慢来,会更快一点,自己掌握好节奏就好. 好了,言归正传. 反射机制是Java中的一个很强大的特性,可以在运行时获取类的信息,比如说类的父类,接口,全部方法名及参数,全部常量和变量,可以说类在反射面前已经衣不遮体了(咳咳,这是正规车).先举一个小栗子

  • 一文搞懂Java的SPI机制(推荐)

    目录 1 简介 缺点 源码 使用 适用场景 插件扩展 案例 1 简介 SPI,Service Provider Interface,一种服务发现机制. 有了SPI,即可实现服务接口与服务实现的解耦: 服务提供者(如 springboot starter)提供出 SPI 接口.身为服务提供者,在你无法形成绝对规范强制时,适度"放权" 比较明智,适当让客户端去自定义实现 客户端(普通的 springboot 项目)即可通过本地注册的形式,将实现类注册到服务端,轻松实现可插拔 缺点 不能按需

  • 一文搞懂Java MD5算法的原理及实现

    目录 MD5加密简介 MD5加密原理 MD5加密常用方法 MD5加密简介 哈希算法又称散列算法,是将任何数据转换成固定长度的算法的统称. 从本质上讲,MD5也是一种哈希算法,其输出是生成128位的输出结果. 如果输入两个不同的明文,就会输出两个不同的输出值,并且根据输出值,不能得到原始的明文,这个过程是不可逆的. MD5加密原理 MD5算法对512位报文的输入信息进行处理,每个报文被分成16个32位报文. 经过一系列处理后,算法的输出由4个32位的数据包组成,这些数据包级联生成一个128位的哈希

  • 一文搞懂Java中的注解和反射

    目录 1.注解(Annotation) 1.1 什么是注解(Annotation) 1.2 内置注解 1.3 元注解(meta-annotation) 1.4 自定义注解 2.反射(Reflection) 2.1 反射和反射机制 2.2 Class类的获取方式和常用方法 2.3 反射的使用 1.注解(Annotation) 1.1 什么是注解(Annotation) 注解不是程序本身,可以在程序编译.类加载和运行时被读取,并执行相应的处理.注解的格式为"@注释名(参数值)",可以附加在

  • 一文搞懂Java中对象池的实现

    目录 1. 什么是对象池 2. 为什么需要对象池 3. 对象池的实现 4. 开源的对象池工具 5. JedisPool 对象池实现分析 6. 对象池总结 最近在分析一个应用中的某个接口的耗时情况时,发现一个看起来极其普通的对象创建操作,竟然每次需要消耗 8ms 左右时间,分析后发现这个对象可以通过对象池模式进行优化,优化后此步耗时仅有 0.01ms,这篇文章介绍对象池相关知识. 1. 什么是对象池 池化并不是什么新鲜的技术,它更像一种软件设计模式,主要功能是缓存一组已经初始化的对象,以供随时可以

  • 一文搞懂Java桥接方法

    目录 1.桥接方法简介 2. 什么时候会生成桥接方法 3. 为什么生成泛型方法 4. 根据桥接方法获取实际泛型方法 1.桥接方法简介 桥接方法是jdk1.5引入泛型后,为使java泛型方法生成的字节码与jdk1.5版本之前的字节码兼容由编译器自动生成的. 可用method.isBridge()判断method是否是桥接方法,在生成的字节码中会有flags标记 ACC_BRIDGE, ACC_SYNTHETIC ,根据来自深入理解java虚拟机的一张访问标志图可以看到 ACC_BRIDGE表示方法

随机推荐