Java 之 ScheduledThreadPoolExecutor 使用

大纲

概述

Java 中 ScheduledExecutorService 是基于线程池设计的定时任务接口,其中的一个实现类是 ScheduledThreadPoolExecutor。每个调度任务都会分配到线程池中的一个线程去执行,也就是说任务是并发执行的,互不影响。

  • ScheduledThreadPoolExecutor 继承了 ThreadPoolExecutor,可以通过线程池进行任务的管理和调度。
  • ScheduledThreadPoolExecutor 实现 ScheduledExecutorService 接口,实现了一些定时任务处理的方法。

提示

ScheduledThreadPoolExecutor 有两种创建方式(如下所示),且它们的作用是等价的。

  • 第一种创建方式
1
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
  • 第二种创建方式
1
ScheduledExecutorService scheduledExecutorService1 = Executors.newScheduledThreadPool(1);

schedule () 方法

schedule() 方法的作用是,提交一个延迟执行的任务,任务从提交时间算起延迟单位 unitdeplay 时间后执行,提交的不是周期任务,任务只会执行一次。

1
2
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
  • 参数说明:
    • period:间隔时间
    • unit:参数是时间单位
    • command:任务实例
      • 可以实现 Runnable 接口
      • 可以实现 Callable 接口,然后通过 ScheduledFuture 获取任务的执行结果

使用案例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class SchudledExecutorTest {

public static void main(String[] args) {
System.out.println("当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1);

// 使用的 Runnable 实现(匿名函数)
executorService.schedule(() -> {
System.out.println("我是使用schedule()方法执行");
Thread thread = Thread.currentThread();
System.out.println("我是间隔1s执行的任务,线程名称为:" + thread.getName() + ",线程ID为:" + thread.getId() + ",当前时间:" +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}, 1, TimeUnit.SECONDS); // 延 1 秒后开始执行

// 关闭线程池
shutdown(executorService);
}

public static void shutdown(ExecutorService executorService) {
// 模拟程序运行耗时
try {
TimeUnit.SECONDS.sleep(60);
} catch (Exception e) {
e.printStackTrace();
}
// 关闭线程池
if (!executorService.isShutdown()) {
executorService.shutdown();
}
}

}

程序运行的输出结果为:

1
2
3
当前时间为:2019-03-24 21:01:00
我是使用schedule()方法执行
我是间隔1s执行的任务,线程名称为:pool-1-thread-1,线程ID为:27,当前时间:2019-03-24 21:01:01

使用案例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class SchudledExecutorTest {

public static void main(String[] args) {
System.out.println("当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1);

//使用 Callable 实现,该实现有返回值,可以获取任务执行的结果
ScheduledFuture<Integer> future = executorService.schedule(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("我是使用schedule()方法执行");
Thread thread = Thread.currentThread();
System.out.println("我是间隔1s执行的任务,线程名称为:" + thread.getName() + ",线程ID为:" + thread.getId() + ",当前时间:" +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return Integer.MAX_VALUE;
}
}, 1, TimeUnit.SECONDS); // 延 1 秒后开始执行

// 获取任务执行的结果
try {
Integer result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}

// 关闭线程池
shutdown(executorService);
}

public static void shutdown(ExecutorService executorService) {
// 模拟程序运行耗时
try {
TimeUnit.SECONDS.sleep(60);
} catch (Exception e) {
e.printStackTrace();
}
// 关闭线程池
if (!executorService.isShutdown()) {
executorService.shutdown();
}
}

}

程序运行的输出结果为:

1
2
3
4
当前时间为:2019-03-24 21:03:11
我是使用schedule()方法执行
我是间隔1s执行的任务,线程名称为:pool-1-thread-1,线程ID为:27,当前时间:2019-03-24 21:03:12
2147483647

scheduleAtFixedRate () 方法

scheduleAtFixedRate () 方法的作用是,相对起始时间点以固定频率调用指定的任务(Fixed-Rate 任务)。当把任务提交到线程池并延迟 initialDelay 时间后开始执行任务,然后从 initialDelay + period 时间点再次执行,接着在 initialDelay + 2 * period 时间点再次执行,循环往复,直到任务在执行过程中抛出了异常,或者调用了任务的 cancel() 方法取消了任务,或者关闭了线程池。

1
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
  • 参数说明:
    • command:任务实例,实现 Runnable 接口
    • initialDelay:初始延迟时间
    • period:间隔时间
    • unit:时间单位

使用案例一

这里将模拟任务执行的时间(总耗时)小于延迟间隔时间 period 的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SchudledExecutorTest {

public static void main(String[] args) {
System.out.println("当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);

// 以固定频率执行指定的任务
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(
"开始测试,当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 模拟业务执行耗时(小于 period 间隔时间)
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(
"结束测试,当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}, 5, 5, TimeUnit.SECONDS); // 延迟5秒后执行,每隔5秒执行一次

// 关闭线程池
shutdown(scheduledExecutorService);
}

public static void shutdown(ExecutorService executorService) {
// 模拟程序运行耗时
try {
TimeUnit.SECONDS.sleep(60);
} catch (Exception e) {
e.printStackTrace();
}
// 关闭线程池
if (!executorService.isShutdown()) {
executorService.shutdown();
}
}

}

程序运行的输出结果为:

1
2
3
4
5
6
7
当前时间为:2019-03-24 21:14:14
开始测试,当前时间为:2019-03-24 21:14:19
结束测试,当前时间为:2019-03-24 21:14:22
开始测试,当前时间为:2019-03-24 21:14:24
结束测试,当前时间为:2019-03-24 21:14:27
开始测试,当前时间为:2019-03-24 21:14:29
结束测试,当前时间为:2019-03-24 21:14:32

使用案例二

这里将模拟任务执行的时间(总耗时)大于延迟间隔时间 period 的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SchudledExecutorTest {

public static void main(String[] args) {
System.out.println("当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);

// 以固定频率执行指定的任务
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(
"开始测试,当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 模拟业务执行耗时(大于 period 间隔时间)
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(
"结束测试,当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}, 5, 5, TimeUnit.SECONDS); // 延迟5秒后执行,每隔5秒执行一次

// 关闭线程池
shutdown(scheduledExecutorService);
}

public static void shutdown(ExecutorService executorService) {
// 模拟程序运行耗时
try {
TimeUnit.SECONDS.sleep(60);
} catch (Exception e) {
e.printStackTrace();
}
// 关闭线程池
if (!executorService.isShutdown()) {
executorService.shutdown();
}
}

}

程序运行的输出结果为:

1
2
3
4
5
6
7
当前时间为:2019-03-24 21:12:32
开始测试,当前时间为:2019-03-24 21:12:37
结束测试,当前时间为:2019-03-24 21:12:43
开始测试,当前时间为:2019-03-24 21:12:43
结束测试,当前时间为:2019-03-24 21:12:49
开始测试,当前时间为:2019-03-24 21:12:49
结束测试,当前时间为:2019-03-24 21:12:55

总结

在调用 scheduleAtFixedRate() 方法时,任务以固定频率执行,这里分为两种情况:

  • 如果任务执行的时间(耗时)小于 period 延迟间隔时间,那么任务的执行周期为:initialDelayinitialDelay + periodinitialDelay + 2 * period、….。
  • 如果任务执行的时间(耗时)大于 period 延迟间隔时间,那么当任务执行完后,下一次任务将立即执行。也就是说下一次任务不会再按照预期的时间间隔执行,最终任务的执行周期为:initialDelayinitialDelay + taskExecuteTimeinitialDelay + 2 * taskExecuteTime、….。

scheduleWithFixedDelay () 方法

scheduleWithFixedDelay() 方法的作用是,当任务执行完毕后,让其延迟固定时间后再次运行(Fixed-Delay 任务)。其中参数 initialDelay 表示提交任务后延迟多少时间开始执行任务,delay 表示当任务执行完毕后延长多少时间后再次运行任务,unitinitialDelaydelay 的时间单位。任务会一直重复执行,直到任务在执行过程中抛出了异常,或者调用了任务的 cancel() 方法取消了任务,或者关闭了线程池。

1
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
  • 参数说明:
    • command:任务实例,实现 Runnable 接口
    • initialDelay:初始延迟时间
    • delay:延迟间隔时间
    • unit:时间单位

案例一

这里将模拟任务执行的时间(总耗时)小于延迟间隔时间 delay 的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SchudledExecutorTest {

public static void main(String[] args) {
System.out.println("当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);

// 当任务执行完毕后,让其延迟固定时间后再次运行
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println(
"开始测试,当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 模拟业务执行耗时(小于 delay 延迟间隔时间)
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(
"结束测试,当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}, 5, 5, TimeUnit.SECONDS); // 延迟5秒后执行,每隔5秒执行一次

// 关闭线程池
shutdown(scheduledExecutorService);
}

public static void shutdown(ExecutorService executorService) {
// 模拟程序运行耗时
try {
TimeUnit.SECONDS.sleep(60);
} catch (Exception e) {
e.printStackTrace();
}
// 关闭线程池
if (!executorService.isShutdown()) {
executorService.shutdown();
}
}

}

程序运行的输出结果为:

1
2
3
4
5
6
7
当前时间为:2019-03-24 21:47:15
开始测试,当前时间为:2019-03-24 21:47:20
结束测试,当前时间为:2019-03-24 21:47:23
开始测试,当前时间为:2019-03-24 21:47:28
结束测试,当前时间为:2019-03-24 21:47:31
开始测试,当前时间为:2019-03-24 21:47:36
结束测试,当前时间为:2019-03-24 21:47:39

案例一

这里将模拟任务执行的时间(总耗时)大于延迟间隔时间 delay 的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SchudledExecutorTest {

public static void main(String[] args) {
System.out.println("当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);

// 当任务执行完毕后,让其延迟固定时间后再次运行
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println(
"开始测试,当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 模拟业务执行耗时(大于 delay 延迟间隔时间)
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(
"结束测试,当前时间为:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}, 5, 5, TimeUnit.SECONDS); // 延迟5秒后执行,每隔5秒执行一次

// 关闭线程池
shutdown(scheduledExecutorService);
}

public static void shutdown(ExecutorService executorService) {
// 模拟程序运行耗时
try {
TimeUnit.SECONDS.sleep(60);
} catch (Exception e) {
e.printStackTrace();
}
// 关闭线程池
if (!executorService.isShutdown()) {
executorService.shutdown();
}
}

}

程序运行的输出结果为:

1
2
3
4
5
6
7
当前时间为:2019-03-24 21:51:31
开始测试,当前时间为:2019-03-24 21:51:36
结束测试,当前时间为:2019-03-24 21:51:42
开始测试,当前时间为:2019-03-24 21:51:47
结束测试,当前时间为:2019-03-24 21:51:53
开始测试,当前时间为:2019-03-24 21:51:58
结束测试,当前时间为:2019-03-24 21:52:04

总结

在调用 scheduleWithFixedDelay() 方法时,不管任务执行的时间(耗时)是多久,下一次任务的执行时间都是上一次任务执行完后再等待延迟间隔 delay 时间后才执行。简而言之,每次执行时间为上一次任务结束起向后推一个时间间隔,即任务的执行周期为:initialDelayinitialDelay + taskExecuteTime + delayinitialDelay + 2 * taskExecuteTime + 2 * delay、…。

参考资料