这里写目录标题

  • 18.3 定时任务的那些陷阱
  • 18.3.1 Timer和TimerTask
  • 1.基本用法
  • 2.基本示例
  • 3.基本原理
  • 4.死循环
  • 5.异常处理
  • 6.小结
  • 18.3.2 ScheduledExecutorService
  • 1.基本用法
  • 2.基本示例
  • 3.基本原理
  • 18.3.3 小结
  • 参考目录


18.3 定时任务的那些陷阱

      本节探讨定时任务,定时任务的应用场景是非常多的,比如:

      ·闹钟程序或任务提醒,指定时间叫床或在指定日期提醒还信用卡。

      ·监控系统,每隔一段时间采集下系统数据,对异常事件报警。

      ·统计系统,一般凌晨一定时间统计昨日的各种数据指标。

      在Java中,主要有两种方式实现定时任务:

      ·使用java.util包中的TimerTimerTask

      ·使用Java并发包中的ScheduledExecutorService

      它们的基本用法都是比较简单的,但如果对它们没有足够的了解, 则很容易陷入其中的一些陷阱。下面,我们就来介绍它们的用法、原理 以及那些陷阱。

18.3.1 Timer和TimerTask

      我们先介绍它们的基本用法和示例,然后介绍它们的实现原理和一 些注意事项。

1.基本用法

      TimerTask表示一个定时任务,它是一个抽象类,实现了 Runnable,具体的定时任务需要继承该类,实现run方法。Timer是一个具体类,它负责定时任务的调度和执行,主要方法有:

开启异步延时线程 java java异步定时任务_开启异步延时线程 java

      需要注意固定延时fixed-delay)与固定频率fixed-rate)的区别,二者都是重复执行,但后一次任务执行相对的时间是不一样的,对于固定延时,它是基于上次任务的“实际”执行时间来算的, 如果由于某种原因,上次任务延时了,则本次任务也会延时,而固定频率会尽量补够运行次数。

      另外,需要注意的是,如果第一次计划执行的时间firstTime是一个 过去的时间,则任务会立即运行,对于固定延时的任务,下次任务会基 于第一次执行时间计算,而对于固定频率的任务,则会从firstTime开始 算,有可能加上period后还是一个过去时间,从而连续运行很多次,直 到时间超过当前时间。我们通过一些简单的例子具体来看下。

2.基本示例

      看一个最简单的例子,如代码清单18-3所示。

      代码清单18-3 Timer基本示例

开启异步延时线程 java java异步定时任务_java_02

      创建一个Timer对象,1秒钟后运行DelayTask,最后调用Timer的 cancel方法取消所有定时任务

      看一个固定延时的简单例子,如代码清单18-4所示。

开启异步延时线程 java java异步定时任务_定时任务_03


      有两个定时任务,第一个运行一次,但耗时5秒第二个是重复执 行,1秒一次,第一个先运行。运行该程序,会发现,第二个任务只有 在第一个任务运行结束后才会开始运行,运行后1秒一次。如果替换上面的代码为固定频率,即变为代码清单18-5所示。

开启异步延时线程 java java异步定时任务_定时任务_04


      运行该程序,第二个任务同样只有在第一个任务运行结束后才会运 行,但它会把之前没有运行的次数补过来,一下子运行5次,输出类似 下面这样:

开启异步延时线程 java java异步定时任务_定时任务_05

3.基本原理

      Timer内部主要由任务队列和Timer线程两部分组成任务队列是一 个基于堆实现的优先级队列,按照下次执行的时间排优先级。Timer线程负责执行所有的定时任务,需要强调的是,一个Timer对象只有一个 Timer线程, 所以,对于上面的例子,任务会被延迟。

      Timer线程主体是一个循环,从队列中获取任务,如果队列中有任务且计划执行时间小于等于当前时间,就执行它,如果队列中没有任务 或第一个任务延时还没到,就睡眠。如果睡眠过程中队列上添加了新任务且新任务是第一个任务,Timer线程会被唤醒,重新进行检查。

      在执行任务之前,Timer线程判断任务是否为周期任务,如果是, 就设置下次执行的时间并添加到优先级队列中,对于固定延时的任务, 下次执行时间为当前时间加上period,对于固定频率的任务,下次执行 时间为上次计划执行时间 加上period

      需要强调是,下次任务的计划是在执行当前任务之前就做出了的, 对于固定延时的任务,延时相对的是任务执行前的当前时间, 而不是 任务执行后,这与后面讲到的Sched-uledExecutorService的固定延时计算 方法是不同的,后者的计算方法更合乎一般的期望。对于固定频率的任务,延时相对的是最先的计划, 所以,很有可能会出现前面例子中一 下子执行很多次任务的情况。

4.死循环

      一个Timer对象只有一个Timer线程,这意味着,定时任务不能耗时太长,更不能是无限循环。 看个例子,如代码清单18-6所示。

开启异步延时线程 java java异步定时任务_Java_06

      第一个定时任务是一个无限循环,其后的定时任务ExampleTask将 永远没有机会执行。

5.异常处理

      关于Timer线程,还需要强调非常重要的一点:在执行任何一个任 务的run方法时,一旦run抛出异常,Timer线程就会退出,从而所有定时任务都会被取消。 我们看个简单的示例,如代码清单18-7所示。

开启异步延时线程 java java异步定时任务_Java_07

      期望TaskA每秒执行一次,但TaskB会抛出异常,导致整个定时任务被取消,程序终止,屏幕输出为:

开启异步延时线程 java java异步定时任务_java_08


      所以,如果希望各个定时任务不互相干扰,一定要在run方法内捕 获所有异常。

6.小结

      可以看到,Timer/TimerTask的基本使用是比较简单的,但我们需要注意:

      ·后台只有一个线程在运行;

      ·固定频率的任务被延迟后,可能会立即执行多次,将次数补够;

      ·固定延时任务的延时相对的是任务执行前的时间;

      ·不要在定时任务中使用无限循环;

      ·一个定时任务的未处理异常会导致所有定时任务被取消。

18.3.2 ScheduledExecutorService

      由于Timer/TimerTask的一些问题,Java并发包引入了 ScheduledExecutorService,下面我们介绍它的基本用法、基本示例和基本原理。

1.基本用法

      ScheduledExecutorService是一个接口,其定义为:

开启异步延时线程 java java异步定时任务_开启异步延时线程 java_09

      它们的返回类型都是ScheduledFuture,它是一个接口,扩展了 FutureDelayed,没有定义额外方法。这些方法的大部分语义与Timer 中的基本是类似的。对于固定频率的任务,第一次执行时间为 initialDelay后,第二次为initialDelay+period,第三次为initial- Delay+2*period,以此类推。不过,对于固定延时的任务,它是从任务执行后开始算的, 第一次为initialDelay后,第二次为第一次任务执行结束后再加上delay。与Timer不同,它不支持以绝对时间作为首次运行的时间。

      ScheduledExecutorService的主要实现类是 ScheduledThreadPoolExecutor,它是线程池ThreadPoolExecutor的子类, 是基于线程池实现的,它的主要构造方法是:

开启异步延时线程 java java异步定时任务_Java_10


      此外,还有构造方法可以接受参数ThreadFactoryRejectedExecutionHandler,含义与ThreadPoolExecutor一样,我们就不赘述了。

      它的任务队列是一个无界的优先级队列,所以最大线程数对它没有作用即使core-PoolSize设为0,它也会至少运行一个线程。

      工厂类Executors也提供了一些方便的方法,以方便创建 ScheduledThreadPoolExecutor,如下所示:

开启异步延时线程 java java异步定时任务_定时任务_11

2.基本示例

      由于可以有多个线程执行定时任务,一般任务就不会被某个长时间运行的任务所延迟了。比如,对于代码清单18-4所示的 TimerFixedDelay,如果改为代码清单18-8所示:

开启异步延时线程 java java异步定时任务_Java_12


      再次执行,第二个任务就不会被第一个任务延迟了。

      另外,与Timer不同,单个定时任务的异常不会再导致整个定时任务被取消,即使后台只有一个线程执行任务。我们看个例子,如代码清 单18-9所示。

开启异步延时线程 java java异步定时任务_Java_13

      TaskA和TaskB都是每秒执行一次,TaskB两秒后执行,但一执行就 抛出异常,屏幕的输出类似如下:

开启异步延时线程 java java异步定时任务_Timer_14

      这说明,定时任务TaskB被取消了,但TaskA不受影响,即使它们 是由同一个线程执行的。不过,需要强调的是,与Timer不同,没有异 常被抛出,TaskB的异常没有在任何地方体现。所以,与Timer中的任务 类似,应该捕获所有异常。

3.基本原理

      ScheduledThreadPoolExecutor的实现思路与Timer基本是类似的,都有一个基于堆的优先级队列,保存待执行的定时任务,它的主要不同是:

      1)它的背后是线程池,可以有多个线程执行任务。

      2)它在任务执行后再设置下次执行的时间,对于固定延时的任务更为合理。

      3)任务执行线程会捕获任务执行过程中的所有异常,一个定时任务的异常不会影响其他定时任务,不过,发生异常的任务(即使是一个 重复任务)不会再被调度。

18.3.3 小结

      本节介绍了Java中定时任务的两种实现方式:TimerScheduledExecutorService需要特别注意Timer的一些陷阱,实践中建 议使用ScheduledExecutorService

      它们的共同局限是不太胜任复杂的定时任务调度。比如,每周一和 周三晚上18:00到22:00,每半小时执行一次。对于类似这种需求,可 以利用我们之前在第7章介绍的日期和时间处理方法,或者利用更为强大的第三方类库,比如Quartzhttp://www.quartz-scheduler.org/ )。

      在并发应用程序中,一般我们应该尽量利用高层次的服务,比如各 种并发容器、任务执行服务和线程池等,避免自己管理线程和它们之间 的同步。但在个别情况下,自己管理线程及同步是必需的,这时,除了 利用前面章节介绍的synchronized显式锁和条件等基本工具,Java并发包 还提供了一些高级的同步和协作工具,以方便实现并发应用,让我们下 一章来了解它们。

参考目录

绝大多数内容来自于:Java编程的逻辑 作者: 马俊昌(第18章 异步任务执行服务 18.3 定时任务的那些陷阱)

Java官方文档
https://docs.oracle.com/javase/specs/index.html