1.定时任务(Timer)
参考《可伸缩服务架构 框架与中间件 第6章》
1.1 前言
-
什么定时任务
在我们生活中有很多关于定时任务的例子:
- 例如每天闹钟八点响,闹钟响就是一个任务,每天八点响就是一个定时任务
- 例如每天第一趟地铁每天 六点发车,这也是一个定时任务
同样的在计算机中也存在着这样的任务。例如当我们开发一个系统时,想要每天两点同步钉钉的数据
同步钉钉的数据是一个任务,每天两点同步,就是一个定时任务。
1.2 实现方式
定时任务有很多种实现方式,这里我们介绍三种jdk timer
,spring task
,Quartz
,这三种不同的实现的方式有着各自的有点和缺陷。
1.3 JDK Timer
Timer 是 JDK 提供的一个定时器, 存在于 java.util包底下。能够定时调度所拥有的任务(TimerTask)。TimerTask是一个定时任务类,这个类实现了Runable接口。需要定时执行的任务都需要写在 run 方法里面
Timer类源码:
-
构造器
// 创建一个定时器,默认的调用的构造器 public Timer() {this("Timer-" + serialNumber());} // 创建一个以守护进程的方式运行的定时器,如果为 true就是以守护进程方式运行。 这种方式当程序结束时,定时器也会结束 public Timer(boolean isDaemon) {this("Timer-" + serialNumber(), isDaemon);} // 创建一个拥有名字的定时器 public Timer(String name) {thread.setName(name);thread.start();} // 创建一个拥有自己名字的同时以守护进程方式运行的定时器 public Timer(String name, boolean isDaemon) { thread.setName(name); thread.setDaemon(isDaemon); thread.start(); }
其实我们通过源码可以得知,构造器里面的代码都是通过
thread
去操作。我们发现thread
就是一个继承了线程类的对象


所以 java.util.Timer定时器实际上是一个单线程。
-
任务调度方法源码
-
指定的实时间(delay 毫秒)之后,只执行一次
image-20200428094043833.png
-
-
指定的时间内,只执行一次
image-20200428094225614.png
-
在指定的延迟(delay,毫秒)之后第一次执行,然后按照间隔时间(period 毫秒)重复执行
image-20200428094306528.png
-
在指定的时间(firstTime)之后第一次执行,然后按照间隔时间(period 毫秒)重复执行
image-20200428094420232.png
-
在指定的延迟时间(delay) 后执行,然后按照间隔时间(period)重复执行
image-20200428094708888.png
-
执行任务,从源码我们可以知道,是通过加锁的方式去实现任务
image-20200428094921334.png
1.4 测试
-
在1s号指定定时任务,只执行1次
image-20200428094921334.png
-
在1s后执行一次任务,然后每个2秒执行一次
image-20200428095609332.png
当然还有很多方法可以自己去测试
-
单线程
image-20200428095815314.png
当我们有两个任务时,由于timer是单线程,总是要等第一个任务执行完成才会去执行下一个任务。也就是说这个任务是串行的。
1.5 缺陷
通过上面简单的例子我们知道,这种定时器存在着一定的优点和缺陷
- jdk提供,不需要导入额外的依赖
- 任务是串行的,如果前一个任务耗时较久,就会影响后面任务的执行。
- 如果在定时任务(TimerTask)抛出了异常会终止定时器的执行。那么当第一个任务抛出了异常,第二个任务也不会被调度执行。
- 执行的时间依赖于操作系统的时间,如果操作系统的时间不准确,任务执行也会有偏差
1.6 ScheduledExecutor
使用Timer存在上述缺点。因此在jdk 5.0
推出了基于线程池设计的ScheduledExecutor
。其核心思想就是当我们每执行一个定时任务就由线程池里面的一个线程去执行。这样就让任务由串行变成了并行,也不存在着必须等前一个任务执行完成,才能调度后一个任务的尴尬局面。
-
实现类
具体实现类为:
ScheduledThreadPoolExecutor
image-20200428102629453.png
-
源码解析:
-
在执行的延迟时间(delay)执行任务(command),返回
ScheduledFuture
与任务交互,TimeUnit
指定时间单位image-20200428103224929.png
-
-
在执行的延迟时间(delay)执行任务(callable),返回
ScheduledFuture
与任务交互,TimeUnit
指定时间单位image-20200428103255853.png
-
在执行的延迟时间(initalDelay)执行任务(callable),返回
ScheduledFuture
与任务交互,以后每个一定的时间(period) 重复执行 ,TimeUnit
指定时间单位image-20200428103337025.png
-
除此意外还有较多类型的方法就不一一列举了。
-
编码测试
package timer; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class Test2 { public static void main(String[] args) { // 创建线程池 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5); // 执行任务 启动程序后1s执行,以后每个3秒执行一次 scheduledExecutorService.scheduleAtFixedRate(() -> { System.out.println("=====任务1:吃饭 ======"); System.out.println("=====任务1:睡觉 ======"); System.out.println("=====任务1:打豆豆 ======"); },1,3, TimeUnit.SECONDS); // 启动程序 1s后执行,执行一次 scheduledExecutorService.schedule(() -> { System.out.println("=====任务2:C# ======"); System.out.println("=====任务2:java ======"); System.out.println("=====任务2:c++ ======"); },1, TimeUnit.SECONDS); // 执行任务 启动程序后1s执行,以后每个2秒执行一次 scheduledExecutorService.scheduleAtFixedRate(() -> { System.out.println("=====任务3:---- ======"); System.out.println("=====任务3:+++ ======"); System.out.println("=====任务3:*** ======"); },1,2, TimeUnit.SECONDS); } }
image-20200428105044054.png
-
对比
- 与Timer相比,线程池方式可以解决Timer不准确问题。支持多个任务并发执行,互相执行不影响。
- 多线程并发执行,可以起到很好的线程隔离问题,即使有一个任务停止了运行,也不影响另外的任务运行
- ScheduledExecutor 基于 时间延迟实现的,执行任务的方法并没有(Date日期相关的参数),因此本地系统的时间基本上不会影响任务的调度。
- 如果在执行过程中任务抛出了异常,则会终止,但是不会影响其他任务的调度。
网友评论