概述
java.util.Timer简介
Timer是用于管理在后台执行的延迟任务或周期性任务,其中的任务使用java.util.TimerTask表示。任务的执行方式有两种:
按固定速率执行:即scheduleAtFixedRate的两个重载方法
按固定延迟执行:即schedule的4个重载方法
我们通过源码来分析它的特性
首先,看一个例子:
public static void main(String[] args) throws Exception {
Timer timer = new Timer();
Thread.sleep(10000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("haha");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 1000, 1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("heihei");
}
}, 1000,1000);
}
这个例子中,我们在主线程中实例化了一个Timer对象,然后在该对象中添加了两个任务。
那么这些任务是怎么执行的?有什么样的一些特性?我们通过源码一步步分析,来梳理该组件的一些特点。
我们从Timer初始化开始分析:
Timer内部有两个属性
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);
有一个属性queue,是用来存储任务的,我们上面的例子中,添加了两个任务,其实就是new 了两个TimeTask对象,放在了这个queue中。
另一个属性是thread,在初始化的时候就会启动该线程:
public Timer(String name) {
thread.setName(name);
thread.start();
}
也就是说在在实例化Timer的同时,我们系统中就生成了一个新的线程,接下来看下该线程的run方法。该TimerThread的run方法中就是一个mianLoop方法:
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
下面逐行解释一下mainLoop的代码含义
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
//如果queue是空的,会一直等待下去,直到Timer添加任务时notify后被唤醒
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
//如果这个任务被取消了,就从queue中移除
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
//如果下次执行的时间早于当前时间,则说明达到了执行的时间点,即taskFired为true
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
//如果period是0,代表该任务不是一个重复执行的任务,可以从queue中移除
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
//如果是重复任务,则修改下次执行的时间:如果period为正数,代表以固定的频率执行任务,如果是负数,代表以固定的延迟时间执行任务
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
//如果下次执行时间晚于当前时间,则等待
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
//如果下次执行的时间早于当前时间,则说明达到了执行的时间点,可以执行任务
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}
这个方法整个就是一个大的死循环。
总结这些对象及属性引用关系如下,Timer对象中有queue,添加任务的时候其实就是在queue中添加一个TimeTask对象,而Timer对象和TimerThread引用的是同一个queue,而ThimeThread的mainLoop方法中的逻辑的不断循环判断queue中各个TimeTask对象的状态,进而进行相应的逻辑处理
上面是实例化Timer的源码分析,总结就是:在实例化的同时,产生了一个新的线程,这个线程在一个大循环中,在不断的轮询queue,如果这个queue是空的,改线程就会调wait方法等待。
下面是添加任务的方法,在添加任务的时候,会判断queue之前是不是为空,如果为空,就说明执行任务的线程在wait,需要进行唤醒。
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.");
// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
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);
//这里结合TimerThread的mainLoop方法看,如果当前加入的task是queue中最小的那个,说明之前的queue是空的,TimerThread在wait,需要被唤醒
if (queue.getMin() == task)
queue.notify();
}
}
void rescheduleMin(long newTime) {
queue[1].nextExecutionTime = newTime;
fixDown(1);
}
重复任务需要重新计算下次执行的时间,然后通过fixDown对任务进行重排,将其顺序与后面的任务进行对比下。这种方式并非排序,而是找到一个合适的位置来交换,因为并不是通过队列逐个找的,而是每次移动一个二进制为,例如传入1的时候,接下来就是2、4、8、16这些位置,找到合适的位置放下即可,顺序未必是完全有序的,它只需要看到距离调度部分的越近的是有序性越强的时候就可以了,这样即可以保证一定的顺序性,达到较好的性能。
通过以上源码,尤其是大循环内的逻辑解释,可以得出如下结论:
1、由于执行任务的线程只有一个,所以如果某个任务的执行时间过长,那么将破坏其他任务的定时精确性。如一个任务每1秒执行一次,而另一个任务执行一次需要5秒,那么如果是固定速率的任务,那么会在5秒这个任务执行完成后连续执行5次,而固定延迟的任务将丢失4次执行。
2、如果执行某个任务过程中抛出了异常,那么执行线程将会终止,导致Timer中的其他任务也不能再执行。
3、Timer使用的是绝对时间,即是某个时间点,所以它执行依赖系统的时间,如果系统时间修改了的话,将导致任务可能不会被执行。
更好的替代方法
由于Timer存在上面说的这些缺陷,在JDK1.5中,我们可以使用ScheduledThreadPoolExecutor来代替它,使用Executors.newScheduledThreadPool工厂方法或使用ScheduledThreadPoolExecutor的构造函数来创建定时任务,它是基于线程池的实现,不会存在Timer存在的上述问题,当线程数量为1时,它相当于Timer。
多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题。
由于执行任务的线程只有一个,所以如果某个任务的执行时间过长,那么将破坏其他任务的定时精确性。
彩蛋:
虽然Timer有很多缺点,但是在分析代码的过程中,发现有一些编码的思想值得学习。
1. 在做循环逻辑的时候,可以更多的使用wait和notify做多线程的交互,减少无用的CPU消耗
2. 在移除某个任务的时候,把列表最后一个任务覆盖到待删除任务的位置,然后最后一个引用被置为null,极大减小了操作的时间复杂度。
void removeMin() { queue[1] = queue[size]; queue[size--] = null; // Drop extra reference to prevent memory leak fixDown(1); }
3. TaskQueue实现了优先队列的数据结构,内部是一个数组,数组内容实际上是从下标1开始填充的;它其实是用balanced binary heap
来表示的,设父节点是queue[n]
,则它的两个字节点分别是queue[2*n]
和queue[2*n+1]。在合适的场景使用合适的数据结构,可以更小成本的实现场景需求。
参考:
https://www.cnblogs.com/heqiyoujing/p/10416065.html
https://blog.csdn.net/yaomingyang/article/details/82113216
https://blog.csdn.net/xieyuooo/article/details/8607220
https://blog.csdn.net/lfsf802/article/details/41621731
https://blog.csdn.net/xieyuooo/article/details/8607220/
https://segmentfault.com/a/1190000009246096
最后
以上就是痴情小熊猫为你收集整理的Timer源码分析的全部内容,希望文章能够帮你解决Timer源码分析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复