知乎上有人问这个问题,借助这里回答下
Java应用如何实现定时器功能?
这里介绍三种Java定时器的实现方式,这三个方式都离不开这个原理,就是下面这个答主回答的
Quartz 的定时任务的定时是如何实现的?www.zhihu.com
不过定时一般是使用时间轮(time wheeel)算法实现。
时间轮算法简单来说可以用下图表示,其主体是一个循环列表,
新任务加入时,会根据目前指针所在位置和需要等待的时间,确定保存在时钟的哪个位置。
时间轮有3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如 当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中秒针走动完全类似了,我们就用这种情况举例子。
箭头运行到一个位置时,就运行相应的任务,然后通过sleep将时间补足一秒,正好就开始下一个tick了。这样循环往复,就可以让每个任务在需要的时间执行。
下面介绍一下三种方式:
1.Timer java.util.Timer
这个是java的JDK源码提供的工具类,可以实现定时的任务。
final
上面的例子延迟10秒钟执行一个打印任务
事实上该程序执行完毕后程序并没有自动结束,因为Timer启动了一个线程,非守护线程,且没有关闭。
事实上如果仅仅是如下的代码
final
程序仍旧是没有关闭的,所以Timer对象当被new出来的时候,就启动了一个线程;
那Timer内部的原理是什么呢?
首先是线程模型,
public
Timer对象有两个重要的成员变量,TaskQueue 用于存放任务 TimerThread 就是那个导致上面程序没有终止的非守护线程,也是派发TaskQueue中的定时任务的线程。
/**
Timer的构造方法,我们看到直接启动了该线程并给线程命了名
class
我们可以看到TaskQueue类和TimerThread类的逻辑也很简单,TaskQueue维护了一个最小堆,TimerThread线程类的run方法将会不断轮询TaskQueue是否有可以执行的任务,当当前没有可执行任务的时候,线程将会主动阻塞,并且阻塞的时间是当前任务队列最早执行的任务的时间减去当前时间,并且当有新的任务加入的时候,会主动唤醒阻塞,比如
private
另外关于Timer类执行耗时任务的并发问题,延迟问题和执行可能抛出非中断异常的任务时的中断问题,我认为是使用不当造成的。
关于网上经常讨论到的Java原生Timer类的缺陷问题,是不是值得商榷?www.zhihu.com真正的任务执行根本不能放到TimerTask的run方法中真正执行,而应该异步出去。
2.ScheduledThreadPoolExecutor
java.util.concurrent.ScheduledThreadPoolExecutor
定时线程池提供的定时器功能本质上模式和上面的Timer模式是类似的,不过主动给我们提供了将任务异步到线程池的功能,禁止将耗时任务在mainloop的线程中执行,mainloop线程仅仅用于派发到时间的任务。
值得注意的是并没有采用TaskQueue的方式来获取最早应该执行的任务,而是使用了JDK提供的并发包,事实上原理是类似的,都是维护一个最小堆,只不过采用支持泛型的新的工具类,且处理并发由重量级的synchronized改为ReentrantLock类的Condition来实现。
具体实现可以参考源码:
java.util.concurrent.DelayQueue
java.util.PriorityQueue
下面是ScheduledThreadPoolExecutor的内部类,包装了DelayQueue的相关操作,
/**
其中被包装的DelayQueue的实现如下,
public
3.quartz
现在回到最上面问题 问到的quartz,其实你也应该想到了,quartz和上面介绍的实现原理大同小异。
我们来看一个最简单的quartz的例子
package
下面我们看下org.quartz.impl.StdScheduler类的start方法做了什么事情?
public
sched.start();调用的其父类的方法如下
public
我们看到其中有一句代码为schedThread.togglePause(false);
我们找到这个schedThread类的定义
package
public
也是一个Thread,并且我们看下这个线程类的初始化就明白了该类在QuartzScheduler初始化的时候就启动了,QuartzSchedulerThread的构造方法自己启动了自己,不过由于没有任务被阻塞了,这时候的start就是将阻塞唤醒。
QuartzSchedulerThread的run方法也是一个for循环,从任务数据中找到应该执行的任务丢到线程池中去执行。
具体到线程池,根据这个调用关系也能发现,一开始就初始化好了。
具体的是就是自定义的SimpleThreadPool类
那么获取最先应该执行的任务的逻辑呢?
public
class
使用TreeMap,将执行时间抽象为一个可比较的类,进行比较,同时保存了时间类对应的任务的映射。
大体是这样,事实上quartz除了可以无脑配置之外,方便定义各种时间,年 月 日 周 外,性能一点也没有比java原生的定时器类有优越之处。