1. 定时任务的应用场景有哪些
- 闹钟程序或任务提醒,指定时间叫起床或在指定日期提醒还信用卡
- 监控系统,每隔一段时间采集系统数据,对异常事件报警
- 统计系统,一般凌晨一定时间统计昨日的各种数据指标
2. Java 中实现定时任务的方式有
- 使用
java.util
包中的Timer
和TimerTask
- 使用 Java 并发包中的
ScheduledExecutorService
(实践中建议使用)
3. Timer
和 TimerTask
的区别是
TimerTask
表示一个定时任务,是一个抽象类。实现了Runnable
,具体的定时任务需要继承该类,实现run()
方法Timer
是一个具体类,负责定时任务的调度和执行
4. Timer
的主要方法有哪些
public void schedule(TimerTask task, Date time)
:在指定绝对时间time
运行任务task
public void schedule(TimerTask task, long delay)
:在当前时间延时delay
毫秒后运行任务task
public void schedule(TimerTask task, Date firstTime, long period)
:固定延时重复执行,第一次计划执行时间为firstTime
,后一次的计划执行时间为前一次“实际”执行时间加上period
public void schedule(Timertask task, long delay, long period)
:固定延时重复执行,第一次执行时间为当前时间加上delay
public void scheduledAtFixedRate(TimerTask task, Date firstTime, long period)
:固定频率重复执行,第一次计划执行时间为firstTime
,后一次的计划执行时间为前一次“计划”执行时间加上period
public void scheduleAtFixedRate(TimerTask task, long delay, long period)
:固定频率重复执行,第一次计划执行时间为当前时间加上delay
5. 固定延时 fixed-delay
和 固定频率 fixed-rate
的区别是
二者都是重复执行,但后一次任务执行相对的时间是不一样的
- 对于固定延时,它是基于上次任务的“实际”执行时间来算的,如果由于某种原因,上次任务延时了,则本次任务也会延时
- 固定频率会尽量补够运行次数(可能会出现一下子执行很多次任务的情况)
另外,如果第一次计划执行的时间
firstTime
是一个过去的时间,则任务会立即执行- 对于固定延时的任务,下次任务会基于第一次执行时间计算
- 对于固定频率的任务,则会从
firstTime
开始算,有可能加上period
后还是一个过去时间,从而连续运行很多次,直到时间超过当前时间
6. 写一个 Timer
基本 Demo
1 | public class BasicTimer { |
7. 写一个 Timer
固定延时 Demo
1 | public class TimerFixedDelay { |
8. 写一个 Timer
固定频率 Demo
1 | public class TimerFixedRate { |
9. Timer
的基本原理
Timer
内部主要由任务队列和Timer
线程两部分组成。任务队列是一个基于堆实现的优先级队列,按照下次执行的时间排优先级。Timer
线程负责执行所有的定时任务,需要强调的是,一个Timer
对象只有一个Timer
线程Timer
线程主体是一个循环,从队列中获取任务,如果队列中有任务且计划执行时间小于当前时间,就执行它,如果队列中没有任务或第一个任务延时还没到,就睡眠。如果睡眠过程中队列上添加了新任务且新任务是第一个任务,Timer
线程会被唤醒,重新进行检查- 在执行任务之前,
Timer
线程判断任务是否为周期任务。如果是,就设置下次执行的时间并添加到优先级队列中。对于固定延时的任务,下次执行时间为当前时间加上period
;对于固定频率的任务,下次执行时间为上次计划执行时间加上period
- 需要强调的是,下次任务的计划是在执行当前任务之前就做出了的。对于固定延时的任务,延时相对的是任务执行前的当前时间,而不是任务执行后。这与后面讲到的
ScheduledExecutorService
的固定延时计算方法是不同的,后者的计算方法更合乎一般的期望;对于固定频率的任务,延时相对的是最先的计划,所以,很有可能出现一下子执行很多次任务的情况 - 一个
Timer
对象只有一个Timer
线程。这意味着,定时任务不能耗时太长,更不能是无限循环(infinite loop)
10. 写一个 Timer
死循环 Demo
1 | public class EndlessLoopTimer { |
11. Timer
线程的异常处理是怎样的,怎样解决
- 对于
Timer
线程,在执行任何一个任务的run()
方法时,一旦run()
抛出异常,Timer
线程就会退出,从而所有定时任务都会被取消 - 如果希望各个定时任务不互相干扰,一定要在
run()
方法内捕获所有异常
12. 写一个 Timer
异常 Demo
1 | public class TimerException { |
1 | // 屏幕输出为: |
13. 总结一下 Timer/TimerTask
- 后台只有一个线程在运行
- 固定频率的任务被延迟后,可能会立即执行多次,将次数补够
- 固定延时任务的延时相对的是任务执行开始前的时间
- 不要在定时任务中使用无限循环,否则会造成死循环
- 一个定时任务的未处理异常会导致所有定时任务被取消
14. ScheduledExecutorService
的使用场景是
- 由于
Timer/TimerTask
的一些问题,Java 并发包引入了ScheduledExecutorService
15. ScheduledExecutorService
接口的定义是
1 | //返回类型都是 ScheduledFuture,它是一个接口,扩展了 Future 和 Delayed,没有定义额外方法 |
16. ScheduledExecutorService
的基本用法
ScheduledExecutorService
的主要实现类是ScheduledThreadPoolExecutor
,它是线程池ThreadPoolExecutor
的子类,是基于线程池实现的,主要构造方法与ThreadPoolExecutor
一样ScheduledThreadPoolExecutor
的任务队列是一个无界的优先级队列,所以最大线程数对它没有作用,因为线程个数最多只能达到corePoolSize
,即使corePoolSize
设为 0,它也会至少运行一个线程- 工厂类
Executors
也提供了一些方便的方法,以方便创建ScheduledThreadPoolExecutor
,如下public static ScheduledExecutorService newSingleThreadScheduledExecutor()
:单线程的定时任务执行服务public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory)
:带参数的单线程的定时任务执行服务public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
:多线程的定时任务执行服务public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
:带参数的多线程的定时任务执行服务
17. 写一个多线程的定时任务执行 Demo
1 | public class ScheduledFixedDelay { |
18. 写一个 ScheduledExecutorService
异常 Demo
1 | public class ScheduledException { |
1 | // 输出类似如下: |
- 定时任务 Task B 被取消了,但 Task A 不受影响,即使它们是由同一个线程执行的
- 与
Timer
不同,没有异常被抛出,Task B 的异常没有在任何地方体现。所以,与Timer
中的任务类似,应该捕获所有异常
19. ScheduledThreadPoolExecutor
类、ScheduledExecutorService
接口和 Timer
类、TimerTask
抽象类的相同点和不同点以及共同局限是
相同点
ScheduledThreadPoolExecutor
的实现思路与Timer
基本是类似的,都有一个基于堆的优先级队列,保存待执行的定时任务
不同点
ScheduledExecutorService
接口不支持以绝对时间作为首次运行的时间ScheduledExecutorService
对于固定的任务,是从任务执行结束后开始算的,即:它在任务执行后再设置下次执行的时间,这对于固定延时的任务更为合理ScheduledThreadPoolExecutor
的单个定时任务的异常不会再导致整个定时任务被取消,即使后台只有一个线程执行任务。即,任务执行线程会捕获任务执行过程中的所以异常,一个定时任务的异常不会影响其他定时任务。不过,发生异常的任务(即使是一个重复任务)不会再被调度
局限:共同局限是不太胜任复杂的定时任务调度。比如,每周一和周三晚上 18:00 到 22:00,每半小时执行一次。对于类似这种需求,可以利用日期和时间处理方法,或者使用更为强大的第三方类库,比如 Quartz: http://www.quartz-scheduler.org/
20. 并发开发小结
- 在并发应用程序中,一般我们应该尽量利用高层次的服务,比如各种并发容器、任务执行服务和线程池等,避免自己管理线程和它们之间的同步
- 但在个别情况下,自己管理线程及同步又是必需的。这时,除了可以利用
synchronized
、显示锁和条件等基本工具,还可以使用 Java 并发包提供的一些高级的同步和协作工具