java定时器的使用(timer)_Timer,ScheduledThreadPoolExecutor与quartz原理介绍

Quartz 的定时任务的定时是如何实现的?​www.zhihu.com

知乎上有人问这个问题,借助这里回答下

Java应用如何实现定时器功能?

这里介绍三种Java定时器的实现方式,这三个方式都离不开这个原理,就是下面这个答主回答的

Quartz 的定时任务的定时是如何实现的?​www.zhihu.com
b2a2eea29d47f120e5d9928821841488.png


不过定时一般是使用时间轮(time wheeel)算法实现。
时间轮算法简单来说可以用下图表示,其主体是一个循环列表,

b1600252ee4eb44056b37f9d997c6227.png

新任务加入时,会根据目前指针所在位置和需要等待的时间,确定保存在时钟的哪个位置。
时间轮有3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如 当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中秒针走动完全类似了,我们就用这种情况举例子。
箭头运行到一个位置时,就运行相应的任务,然后通过sleep将时间补足一秒,正好就开始下一个tick了。这样循环往复,就可以让每个任务在需要的时间执行。

下面介绍一下三种方式:

1.Timer java.util.Timer

这个是java的JDK源码提供的工具类,可以实现定时的任务。

final 

上面的例子延迟10秒钟执行一个打印任务

50e23f9fcf876d52387778874d32a4b3.png

事实上该程序执行完毕后程序并没有自动结束,因为Timer启动了一个线程,非守护线程,且没有关闭。

事实上如果仅仅是如下的代码

final 

f568585bc4044e8af1a102db7c296c2f.png

程序仍旧是没有关闭的,所以Timer对象当被new出来的时候,就启动了一个线程;

那Timer内部的原理是什么呢?

首先是线程模型,

public 

Timer对象有两个重要的成员变量,TaskQueue 用于存放任务 TimerThread 就是那个导致上面程序没有终止的非守护线程,也是派发TaskQueue中的定时任务的线程。

/**

Timer的构造方法,我们看到直接启动了该线程并给线程命了名

class 

我们可以看到TaskQueue类和TimerThread类的逻辑也很简单,TaskQueue维护了一个最小堆,TimerThread线程类的run方法将会不断轮询TaskQueue是否有可以执行的任务,当当前没有可执行任务的时候,线程将会主动阻塞,并且阻塞的时间是当前任务队列最早执行的任务的时间减去当前时间,并且当有新的任务加入的时候,会主动唤醒阻塞,比如

private 

另外关于Timer类执行耗时任务的并发问题,延迟问题和执行可能抛出非中断异常的任务时的中断问题,我认为是使用不当造成的。

关于网上经常讨论到的Java原生Timer类的缺陷问题,是不是值得商榷?​www.zhihu.com
d1c77a7b072b7e295a5c761e9d6a4acd.png

真正的任务执行根本不能放到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 

6252f0c4f60729da79800fcc2252a107.png

下面我们看下org.quartz.impl.StdScheduler类的start方法做了什么事情?

public 

sched.start();调用的其父类的方法如下

public 

我们看到其中有一句代码为schedThread.togglePause(false);

我们找到这个schedThread类的定义

package 
public 

也是一个Thread,并且我们看下这个线程类的初始化就明白了该类在QuartzScheduler初始化的时候就启动了,QuartzSchedulerThread的构造方法自己启动了自己,不过由于没有任务被阻塞了,这时候的start就是将阻塞唤醒。

QuartzSchedulerThread的run方法也是一个for循环,从任务数据中找到应该执行的任务丢到线程池中去执行。

e001b06ecbeecd301082faca81c65557.png

具体到线程池,根据这个调用关系也能发现,一开始就初始化好了。

15a7ecc1abae0518099705bfe30be29d.png

具体的是就是自定义的SimpleThreadPool类

那么获取最先应该执行的任务的逻辑呢?

7f61d00d9d21aa62de1309327c5dae52.png

be24a7eb4032ac0d1c64189e04c533a3.png
public 
class 

使用TreeMap,将执行时间抽象为一个可比较的类,进行比较,同时保存了时间类对应的任务的映射。

大体是这样,事实上quartz除了可以无脑配置之外,方便定义各种时间,年 月 日 周 外,性能一点也没有比java原生的定时器类有优越之处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值