在jdk自带的库中,有两种技术可以实现定时任务。一种是使用Timer,另外一个则是ScheduledThreadPoolExecutor。下面为大家分析一下这两个技术的底层实现原理以及各自的优缺点。
package java.util;
Timer 可以创建一次Timer 对象 ,但可以多次调用schedule(方法)添加任务到队列。 由TimerThread 线程循环执行队列中的任务。 保证任务线程循环执行任务队列。 单个TimerThread 线程在处理。
一、Timer
1. Timer的使用
class MyTask extends TimerTask{
@Override
public void run() {
System.out.println("hello world");
}
}
public class TimerDemo {
public static void main(String[] args) {
//创建定时器对象
Timer t=new Timer();
//在3秒后执行MyTask类中的run方法,后面每10秒跑一次
t.schedule(new MyTask(), 3000,10000);
}
}
通过往Timer提交一个TimerTask的任务,同时指定多久后开始执行以及执行周期,我们可以开启一个定时任务。
2. 源码解析
首先我们先来看一下Timer这个类
//存放定时任务的队列
//这个TaskQueue 也是Timer内部自定义的一个队列,这个队列通过最小堆来维护队列
//下一次执行时间距离现在最小的会被放在堆顶,到时执行线程直接获取堆顶任务并判断是否执行即可
private final TaskQueue queue = new TaskQueue();
//负责执行定时任务的线程
private final TimerThread thread = new TimerThread(queue);
public Timer() {
this("Timer-" + serialNumber());
}
public Timer(String name) {
//设置线程的名字,并且启动这个线程
thread.setName(name);
thread.start();
}
再来看一下TimerThread 这个类,这个类也是定义在Timer.class中的一个类,它继承了Thread类,所以可以直接拿来当线程使用。
我们直接来看他的构造方法以及run方法
//在Timer中初始化的时候会将Timer的Queue赋值进来
TimerThread(TaskQueue queue) {
this.queue = queue;
}
public void run() {
try {
//进入自旋,开始不断的从任务队列中获取定时任务来执行
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
//加同步
synchronized(queue) {
//如果任务队列为空,并且newTasksMayBeScheduled为true,就休眠等待,直到有任务进来就会唤醒这个线程
//如果有人调用timer的cancel方法,newTasksMayBeScheduled会变成false
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break;
// 获取当前时间和下次任务执行时间
long currentTime, executionTime;
//获取队列中最早要执行的任务
task = queue.getMin();
synchronized(task.lock) {
//如果这个任务已经被结束了,就从队列中移除
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
//获取当前时间和下次任务执行时间
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
//判断任务执行时间是否小于当前时间,表示小于,就说明可以执行了
if (taskFired = (executionTime<=currentTime)) {
//如果任务的执行周期是0,说明只要执行一次就好了,就从队列中移除它,这样下一次就不会获取到该任务了
if (task.period == 0) {
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else {
//重新设置该任务下一次的执行时间
//如果之前设置的period小于0,就用当前时间-period,等于就是当前时间加上周期值
//这里的下次执行时间就是当前的执行时间加上周期值
//这里涉及到是否以固定频率调用任务的问题,下面再详细讲解
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
//如果任务的执行时间还没到,就计算出还有多久才到达执行时间,然后线程进入休眠
if (!taskFired)
queue.wait(executionTime - currentTime);
}
//如果任务的执行时间到了,就执行这个任务
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}
通过上面的代码,我们大概了解了Timer是怎么工作的了。下面来看一下schedule()方法的相关代码
//Timer.java
public void schedule(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
//调用内部的一个方法
sched(task, System.currentTimeMillis()+delay, -period);
}
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// 如果设定的定时任务周期太长,就将其除以2
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
//加锁同步
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
//设置任务的各个属性
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
//将任务加入到队列中
queue.add(task);
//如果任务加入队列后排在堆顶,说明该任务可能马上可以执行了,那就唤醒执行线程
if (queue.getMin() == task)
queue.notify();
}
}
3. 总结
Timer的原理比较简单,当我们初始化Timer的时候,timer内部会启动一个线程,并且初始化一个优先级队列,该优先级队列使用了最小堆的技术来将最早执行时间的任务放在堆顶。
当我们调用schedule方法的时候,其实就是生成一个任务然后插入到该优先级队列中。最后,timer内部的线程会从优先级队列的堆顶获取任务,获取到任务后,先判断执行时间是否到了,如果到了先设置下一次的执行时间并调整堆,然后执行任务。如果没到执行时间那线程就休眠一段时间。
关于计算下次任务执行时间的策略:
这里设置下一次执行时间的算法会根据传入peroid的值来判断使用哪种策略:
- 如果peroid是负数,那下一次的执行时间就是当前时间+peroid的值
- 如果peroid是正数,那下一次执行时间就是该任务这次的执行时间+peroid的值。
这两个策略的不同点在于,如果计算下次执行时间是以当前时间为基数,那它就不是以固定频率来执行任务的。因为Timer是单线程执行任务的,如果A任务执行周期是10秒,但是有个B任务执行了20几秒,那么下一次A任务的执行时间就要等B执行完后轮到自己时,再过10秒才会执行下一次。
如果策略是这次任务的执行时间+peroid的值就是按固定频率不断执行任务了。读者可以自行模拟一下
---------------------
作者:疯狂哈丘
来源:CSDN
原文:https://blog.csdn.net/u013332124/article/details/79603943
版权声明:本文为博主原创文章,转载请附上博文链接!