Timer 定时器相信都不会陌生,之因此拿它来作源码分析,是发现整个控制流程能够体现不少有意思的东西。html
在业务开发中常常会遇到执行一些简单定时任务的需求,一般为了不作一些看起来复杂的控制逻辑,通常考虑使用 Timer 来实现定时任务的执行,下面先给出一个最简单用法的例子:java
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
// scheduledExecutionTime() 返回此任务最近开始执行的时间
Date date = new Date(this.scheduledExecutionTime());
System.out.println("timeTask run " + date);
}
};
// 从如今开始每间隔 1000 ms 计划执行一个任务
timer.schedule(timerTask, 0, 1000);
复制代码
Timer 能够按计划执行重复的任务或者定时执行指定任务,这是由于 Timer 内部利用了一个后台线程 TimerThread 有计划地执行指定任务。算法
**Timer:**是一个实用工具类,该类用来调度一个线程(schedule a thread),使它能够在未来某一时刻执行。 Java 的 Timer 类能够调度一个任务运行一次或按期循环运行。 Timer tasks should complete quickly. 即定时器中的操做要尽量花费短的时间。segmentfault
**TimerTask:**一个抽象类,它实现了 Runnable 接口。咱们须要扩展该类以便建立本身的 TimerTask ,这个 TimerTask 能够被 Timer 调度。数组
一个 Timer 对象对应的是单个后台线程,其内部维护了一个 TaskQueue,用于顺序执行计时器任务 TimeTask 。安全
TaskQueue 队列,内部用一个 TimerTask[] 数组实现优先队列(二叉堆),默认最大任务数是 128 ,当添加定时任务超过当前最大容量时会这个数组会拓展到原来 2 倍。数据结构
优先队列主要目的是为了找出、返回并删除优先队列中最小的元素,这里优先队列是经过数组实现了平衡二叉堆,TimeQueue 实现的二叉堆用数组表示时,具备最小 nextExecutionTime 的 TimerTask 在队列中为 queue[1] ,因此堆中根节点在数组中的位置是 queue[1] ,那么第 n 个位置 queue[n] 的子节点分别在 queue[2n] 和 queue[2n+1] 。关于优先队列的数据结构实现,这里推荐一篇文章:数据结构与算法学习笔记 - 优先队列、二叉堆、左式堆。多线程
按照 TaskQueue 的描述:This class represents a timer task queue: a priority queue of TimerTasks, ordered on nextExecutionTime.
这是一个优先队列,队列的优先级按照 nextExecutionTime 进行调度。 也就说 TaskQueue 按照 TimerTask 的 nextExecutionTime 属性界定优先级,优先级高的任务先出队列,也就先执行任务调度。并发
如上图所示,列举了优先队列中部分操做的实现,优先队列插入和删除元素的复杂度都是O(logn),因此add, removeMin 和 rescheduleMin
方法的性能都是不错的。从上图能够知道,获取下一个计划执行任务时,取队列的头出列便可,为了减小额外性能消耗,移除队列头部元素的操做是先把队尾元素赋值到队首后,再把队尾置空,队列数量完成减一后进行优先权值操做。再下面看看保证优先队列最核心的两个方法fixUp
和fixDown
。ide
两个方法的核心思路都是经过向上或向下调整二叉堆中元素所在位置,保持堆的有序性: fixUp 是将元素值小于父节点的子节点与父节点交换位置,保持堆有序。交换位置后,原来的子节点可能仍然比更上层的父节点小, 因此整个过程须要循环进行。这样一来,原来的子节点有可能升级为层级更高的父节点,相似于一个轻的物体从湖底往上浮直到达到其重力与浮力相平衡的过程。 fixDown 将元素值大于子节点的父节点与子节点交换位置,交换位置后, 原来的父节点仍然有可能比其下面的子节点大, 因此还须要继续进行类相同的操做,以便保持堆的有序性。因此整个过程循环进行。 这相似于一个重的物体从湖面下沉到距离湖底的某个位置,直到达到其重力与浮力相平衡为止。 总的来讲,就是调整大的元素下沉,小的元素上浮,反复调整后堆顶一直保持是堆中最小的元素,父节点元素要一直小于或等于子节点。
前面说完 Timer 源码中优先队列的实现,下面咱们来看看其若是操做优先队列,实现 TimerTask 的计划调度的:
Timer 提供了四个构造方法,每一个构造方法都启动了一个后台线程(默认不是守护线程,除非主动指定)。因此对于每个 Timer 对象而言,其内部都是对应着单个后台线程,这个线程用于顺序执行优先队列中全部的计时器任务。
当初始化完成 Timer 后,咱们就能够往 Timer 中添加定时任务,而后定时任务就会按照咱们设定的时间交由 Timer 取调度执行。Timer 提供了 schedule 方法,该方法依靠屡次重载的方式来适应不一样的状况,具体以下:
**schedule(TimerTask task, Date time):**安排在指定的时间执行指定的任务。
**schedule(TimerTask task, long delay) :**安排在指定延迟后执行指定的任务。
**schedule(TimerTask task, Date firstTime, long period) :**安排指定的任务在指定的时间开始进行重复的固定延迟执行。
**schedule(TimerTask task, long delay, long period) :**安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。
scheduleAtFixedRate :,scheduleAtFixedRate 方法与 schedule 相同,只不过他们的侧重点不一样,区别后面分析。
**scheduleAtFixedRate(TimerTask task, Date firstTime, long period):**安排指定的任务在指定的时间开始进行重复的固定速率执行。
**scheduleAtFixedRate(TimerTask task, long delay, long period):**安排指定的任务在指定的延迟后开始进行重复的固定速率执行。
首先来看 schedule(TimerTask task, Date time)
和 schedule(TimerTask task, long delay)
,第一个参数传入是定时任务的实例,区别在于方法的第二个参数,date 是在指定的时间点,delay 是当前时间延后多少毫秒。这就引出了 Timer 具备的两个特性:定时(在指定时间点执行任务)和延迟(延迟多少秒后执行任务)。 值得你们注意的是:这里所说时间都是跟系统时间相关的绝对时间,而不是相对时间,基于这点,Timer 对任务的调度计划和系统时间息息相关,因此它对系统时间的改变很是敏感。
下面在来看看 schedule(TimerTask task, Date time)
和 schedule(TimerTask task, Date firstTime, long period)
的区别。对比方法中新增的 period 参数,period 做用区别在于 Timer 的另外一个特性:周期性地执行任务(一次任务结束后,能够每隔个 period 豪秒后再执行任务,如此反复)。
从上面 schedule 的方法重载来看,最终都是调用了 sched(TimerTask task, long time, long period)
方法,只是传入的参数不一样,下面就再来看就看关于 schedule 和 scheduleAtFixedRate 的区别:
从调用方法来看,他们的区别仅仅是传入 sched 方法 period 参数正负数的差异,因此具体的就要看 sched 方法的实现。
能够看到 sched 方法主要是设置 TimerTask 属性和状态,好比 nextExecutionTime 等,而后将任务添加到队列中。能看出来,设置定时任务 task 属性时是加了锁的,并且在添加任务到队列时,这里使用 Timer 内 TaskQueue 实例做为对象锁,而且使用 wait 和 notify 方法来通知任务调度。Timer 类能够保证多个线程能够共享单个 Timer 对象而无需进行外部同步,因此 Timer 类是线程安全的。
这里注意区分开: 前面一个 Timer 对象中用于处理任务调度的后台线程TimerThread 实例和 schedule 方法传入后被加入到 TaskQueue 的 TimerTask 任务的实例,二者是不同的。
要想知道为 TimerTask 设置属性和状态的做用,那就得进一步看看 TimerTask 类的具体实现了。
TimerTask 类是一个抽象类,能够由 Timer 安排为一次执行或重复执行的任务。它有一个抽象方法 run() 方法,用于子类实现 Runnale 接口。能够在 run 方法中写定时任务的具体业务逻辑。
能够看到下图中 TimerTask 类中的文档描述,若是任务是按计划执行,那么 nextExecutionTime 属性是指下次任务的执行时间,时间格式是按照 System.currentTimeMillis 返回的。对于须要重复进行的任务,每一个任务执行以前会更新这一属性。
而 period 属性是用来表示以毫秒为时间单位的重复任务。period 为正值时表示固定速率执行,负值表示固定延迟执行,值 0 表示一个非重复性的任务。
所谓固定速率执行和固定延迟执行,固定延迟指的是定时任务会由于前一个任务的延迟而致使其后面的定时任务延时,而固定速率执行则不会有这个问题,它是直接按照计划的速率重复执行,不会考虑前面任务是否执行完。
这也是 scheduleAtFixedRate 与 schedule 方法的区别,二者侧重点不一样,schedule 方法侧重保存间隔时间的稳定,而 scheduleAtFixedRate 方法更加侧重于保持执行频率的稳定。
另外 TimerTask 还有两个非抽象方法:
说完这些,下面就来看看 Timer 的后台线程具体是如何调度队列中的定时任务,能够看到 TimerThread 是持有任务队列进行操做的,也就具备了任务调度功能了。
下面就来看看后台线程的 run 方法调用 mainLoop 具体作了什么:
前面说到每一个 Timer 对象内部包含一个 TaskQueue 实例,在执行定时任务时,TimerThread 中将这个 taskqueue 对象做为锁,在任什么时候刻只能有一个线程执行 TimerTask 。Timer 类为了保证线程安全的,是不须要外部同步机制就能够共享同一个 Timer 对象。
能够看到 Timer 是不会捕获异常的,若是 TimerTask 抛出的了未检查异常则会致使 Timer 线程终止,同时 Timer 也不会从新恢复线程的执行,它会错误的认为整个 Timer 线程都会取消。同时,已经被安排但还没有执行的 TimerTask 也不会再执行了,新的任务也不能被调度。因此,若是 TimerTask 抛出未检查的异常,Timer 将会产生没法预料的行为。
注意看计划安排任务的核心代码,包括任务计划执行时间的设置,也有优先队列保持二叉堆序性地操做。下面代码很好地体现了 period 属性做用,period 为正值时表示固定速率执行,负值表示固定延迟执行,值 0 表示一个非重复性的任务。
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
复制代码
前面提过 Timer 使用 schedule (TimerTask task, Date firstTime, long period) 方法执行的计时器任务可能会由于前一个任务执行时间较长而延时。每一次执行的 task 的计划时间会随着前一个 task 的实际时间而发生改变,也就是 scheduledExecutionTime(n+1) = realExecutionTime(n) + periodTime。也就是说若是第 n 个 task 因为某种状况致使此次的执行时间过程,最后致使 systemCurrentTime>= scheduledExecutionTime(n+1),这是第 n+1 个 task 并不会由于到时了而执行,他会等待第 n 个 task 执行完以后再执行,那么这样势必会致使 n+2 个的执行时间 scheduledExecutionTime 发生改变。因此 schedule 方法更加注重保存间隔时间的稳定。
而对于 scheduleAtFixedRate(TimerTask task, Date firstTime, long period),在前面也提过 scheduleAtFixedRate 与 schedule 方法的侧重点不一样,schedule 方法侧重保存间隔时间的稳定,而 scheduleAtFixedRate 方法更加侧重于保持执行频率的稳定。在 schedule 方法中会由于前一个任务的延迟而致使其后面的定时任务延时,而 scheduleAtFixedRate 方法则不会,若是第 n 个 task 执行时间过长致使 systemCurrentTime >= scheduledExecutionTime(n+1),则不会作任何等待他会当即执行第 n+1 个 task,因此 scheduleAtFixedRate 方法执行时间的计算方法不一样于 schedule,而是 scheduledExecutionTime(n)=firstExecuteTime +n*periodTime,该计算方法永远保持不变。因此 scheduleAtFixedRate 更加侧重于保持执行频率的稳定。
说完了 Timer 的源码分析,相信大体上也能明白定时集整个流程是怎样的。下面根据上面这些内容,说一些实际使用建议。
最近使用阿里 Java 开发编码规约插件,能够看到提示是建议使用 ScheduledExecutorService 代替 Timer :
那为何要使用 ScheduledExecutorService 代替 Timer :
前面咱们也有提到,Timer 是基于绝对时间的,对系统时间比较敏感,而 ScheduledThreadPoolExecutor 则是基于相对时间;
Timer 是内部是单一线程,而 ScheduledThreadPoolExecutor 内部是个线程池,因此能够支持多个任务并发执行。
Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。
使用 ScheduledExecutorService 更容易明确任务实际执行策略,更方便自行控制。
默认 Timer 执行线程不是 daemon 线程, 任务执行完,主线程(或其余启动定时器的线程)结束时,task 线程并无结束。须要注意潜在内存泄漏问题
下面给出一个实际使用 ScheduledExecutorService 代替 Timer 的例子:
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/** * ImprovedTimer 改进过的定时器 * 多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行, * 使用ScheduledExecutorService则没有这个问题。 * * @author baishixian * @date 2017/10/16 * */
public class ImprovedTimer {
/** * 线程池不容许使用Executors去建立,而是经过ThreadPoolExecutor的方式,这样的处理方式让写的同窗更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端: * 1)newFixedThreadPool和newSingleThreadExecutor: * 主要问题是堆积的请求处理队列可能会耗费很是大的内存,甚至OOM。 * 2)newCachedThreadPool和newScheduledThreadPool: * 主要问题是线程数最大数是Integer.MAX_VALUE,可能会建立数量很是多的线程,甚至OOM。 * * 线程池能按时间计划来执行任务,容许用户设定计划执行任务的时间,int类型的参数是设定 * 线程池中线程的最小数目。当任务较多时,线程池可能会自动建立更多的工做线程来执行任务 */
private final ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new ImprovedTimer.DaemonThreadFactory());
private ScheduledFuture<?> improvedTimerFuture = null;
public ImprovedTimer() {
}
/** * 周期性重复执行定时任务 * @param command 执行 Runnable * @param initialDelay 单位 MILLISECONDS * @param period 单位 MILLISECONDS */
public void schedule(Runnable command, long initialDelay, long period){
// initialDelay 毫秒后开始执行任务,之后每隔 period 毫秒执行一次
// schedule方法被用来延迟指定时间来执行某个指定任务。
// 若是你须要周期性重复执行定时任务可使用scheduleAtFixedRate或者scheduleWithFixedDelay方法,它们不一样的是前者以固定频率执行,后者以相对固定频率执行。
// 无论任务执行耗时是否大于间隔时间,scheduleAtFixedRate和scheduleWithFixedDelay都不会致使同一个任务并发地被执行。
// 惟一不一样的是scheduleWithFixedDelay是当前一个任务结束的时刻,开始结算间隔时间,如0秒开始执行第一次任务,任务耗时5秒,任务间隔时间3秒,那么第二次任务执行的时间是在第8秒开始。
improvedTimerFuture = executorService.scheduleAtFixedRate(command, initialDelay, period, TimeUnit.MILLISECONDS);
}
/** * 周期性重复执行定时任务 * @param command 执行 Runnable * @param initialDelay 单位 MILLISECONDS */
public void schedule(Runnable command, long initialDelay){
// initialDelay 毫秒后开始执行任务
improvedTimerFuture = executorService.schedule(command, initialDelay, TimeUnit.MILLISECONDS);
}
private void cancel() {
if (improvedTimerFuture != null) {
improvedTimerFuture.cancel(true);
improvedTimerFuture = null;
}
}
public void shutdown() {
cancel();
executorService.shutdown();
}
/** * 守护线程工厂类,用于生产后台运行线程 */
private static final class DaemonThreadFactory implements ThreadFactory {
private AtomicInteger atoInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable);
thread.setName("schedule-pool-Thread-" + atoInteger.getAndIncrement());
thread.setDaemon(true);
return thread;
}
}
}
复制代码
参考: 详解 Java 定时任务 Java多线程总结(3)— Timer 和 TimerTask深刻分析
OVER...