在现实世界里,我们总是免不了要定期去做一件事情(比如上课)—— 在计算机的世界里,更是如此。比如我们手机每天叫我们起床的电子闹钟,某些网站会定期向我们发送一些推荐相关的邮件,集群中我们需要每隔一定时间检查是否有机器宕机等。



在 ​使用线程池​ 中已经介绍,JDK 1.5 时,标准类库添加了对线程池的支持,然后在线程池核心实现 ​​ThreadPoolExecutor​​ 的基础上,实现了 ​​ScheduledThreadPoolExecutor​​,作为可以 定时和周期性执行任务 的线程池。​​ScheduledThreadPoolExecutor​​ 的类图如下:

​ScheduledThreadPoolExecutor​​ 实现了 ​​ScheduledExecutorService​​ 接口,​​ScheduledExecutorService​​ 继承了 ​​ExecutorService​​ 接口,所以首先 ​​ScheduledThreadPoolExecutor​​ 是一个 ​​ExecutorService​​ (线程池),然后除了具有线程池的功能,它还有定时和周期性执行任务的功能。​​ScheduledExecutorService​​ 除了从 ​​ExecutorService​​ 继承的方法外,还包括如下四个方法:

第一个 ​​Schedule​​ 方法:

delay 指定的时间后,执行指定的 ​​Runnable​​ 任务,可以通过返回的 ​​ScheduledFuture<?>​​ 与该任务进行交互。

第二个 ​​Schedule​​ 方法:

delay 指定的时间后,执行指定的 ​​Callable<V>​​ 任务,可以通过返回的 ​​ScheduledFuture<V>​​ 与该任务进行交互。

(​​ScheduledFuture​​ 接口 继承自 ​​Future​​ 接口,所以 ​​ScheduledFuture​​ 和任务的交互方式与 ​​Future​​ 一致。所以通过​​ScheduledFuture​​,可以 判断定时任务是否已经完成,获得定时任务的返回值,或者取消任务等)

​scheduleAtFixedRate​​ 方法:

initialDelay 指定的时间后,开始按周期 period 执行指定的 ​​Runnable​​ 任务。

假设调用该方法后的时间点为 ​​0​​,那么第一次执行任务的时间点为 ​​initialDelay​​,第二次为 ​​initialDelay + period​​,第三次为 ​​initialDelay + period + period​​,以此类推。

​scheduleWithFixedDelay​​ 方法:

initialDelay 指定的时间后,开始按指定的 delay 延期性的执行指定的 ​​Runnable​​ 任务。

假设调用该方法后的时间点为 ​​0​​,每次任务需要耗时 ​​T(i)​​(i 为第几次执行任务),那么第一次执行任务的时间点为 ​​initialDelay​​,第一次完成任务的时间点为 ​​initialDelay + T(1)​​,则第二次执行任务的时间点为 ​​initialDelay + T(1) + delay​​;第二次完成任务的时间点为 ​​initialDelay + (T(1) + delay) + T(2)​​,所以第三次执行任务的时间点为 ​​initialDelay + T(1) + delay + T(2) + delay​​,以此类推。



我们来实践下 ​​ScheduledThreadPoolExecutor​​ 的 ​​scheduleAtFixedRate​​ 方法:



1 public class ScheduledExecutorServiceTest {
2
3 public static void main(String[] args) throws Exception {
4 ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
5
6 TimerTask timerTask = new TimerTask(2000); // 任务需要 2000 ms 才能执行完毕
7
8 System.out.printf("起始时间:%s\n\n", new SimpleDateFormat("HH:mm:ss").format(new Date()));
9
10 // 延时 1 秒后,按 3 秒的周期执行任务
11 timer.scheduleAtFixedRate(timerTask, 1000, 3000, TimeUnit.MILLISECONDS);
12 }
13
14 private static class TimerTask implements Runnable {
15
16 private final int sleepTime;
17 private final SimpleDateFormat dateFormat;
18
19 public TimerTask(int sleepTime) {
20 this.sleepTime = sleepTime;
21 dateFormat = new SimpleDateFormat("HH:mm:ss");
22 }
23
24 @Override
25 public void run() {
26 System.out.println("任务开始,当前时间:" + dateFormat.format(new Date()));
27
28 try {
29 System.out.println("模拟任务运行...");
30 Thread.sleep(sleepTime);
31 } catch (InterruptedException ex) {
32 ex.printStackTrace(System.err);
33 }
34
35 System.out.println("任务结束,当前时间:" + dateFormat.format(new Date()));
36 System.out.println();
37 }
38
39 }
40 }


运行结果:

ScheduledThreadPoolExecutor 使用线程池执行定时任务_运行时间

 

可以看到运行结果完全符合预期 —— 延时 1 秒后,每隔 3 秒执行一次任务。



上面是任务的运行时间小于周期时间的情况 —— 那如果任务运行的时间大于给定的执行周期呢?(比如任务运行需要 3 s,但是我们指定的周期为 2 s)

修改 ​​main​​ 方法:

 



1 public static void main(String[] args) throws Exception {
2 ScheduledExecutorService timer = Executors.newScheduledThreadPool(2);
3
4 TimerTask timerTask = new TimerTask(3000); // 每个任务需要 3000 ms 才能执行完毕
5
6 System.out.printf("起始时间:%s\n\n", new SimpleDateFormat("HH:mm:ss").format(new Date()));
7
8 timer.scheduleAtFixedRate(timerTask, 1000, 2000, TimeUnit.MILLISECONDS);
9 }


运行结果:

ScheduledThreadPoolExecutor 使用线程池执行定时任务_线程池_02

 

 

可以看到此时虽然我们指定的周期为 2 s,但是因为任务的运行就需要 3 s(超过周期),所以这种情况下 ​​scheduleAtFixedRate​​ 的处理方式为 上一次任务刚完成,则紧接着立即运行下一次任务,而不是使用线程池中的空闲线程来运行任务以维护 2 秒这个周期 —— 由此可见,每个定时任务在 ​​ScheduledThreadPoolExecutor​​ 中,都是串行运行的,即下一次运行任务一定在上一次任务结束之后。

(​​scheduleWithFixedDelay​​ 方法 的使用也十分简单,请有兴趣的读者自己实践)