java.util.Timer:线程的工具,用于在后台线程中安排将来执行的任务。可以安排一次性执行任务,或定期重复执行任务。任务应该是能快速完成的。这个类是线程安全的:多线程可以共享一个Timer对象,而不需要外部同步。
这个类可以扩展到大量同时计划的任务(数千个都没有问题)。在内部,它使用二进制堆来表示其任务队列,因此调度任务的成本为O(log n),其中n是并发计划任务的数量。
下面基于JDK1.8源码全面分析Timer的实现。
来看一下Timer的属性:
TaskQueue queue = new TaskQueue();//任务队列
TimerThread thread = new TimerThread(queue);//持有任务队列的Thread。
先简单看一下TaskQueue和TimerThread的定义。
TaskQueue:表示一个计时器任务队列,一个TimerTasks的优先级队列,通过nextExecutionTime字段排序,内部使用堆实现。
属性:
TimerTask[]queue = new TimerTask[128];//采用堆排序的数组
int size = 0;代表优先队列中任务数;
TimerThread:继承自Thread。相当于一个“助手类”,实现了定时器的任务执行线程,该线程功能包括等待定时器队列上的任务,在它们触发时执行它们,重新安排重复任务,并删除取消的任务,并从队列中去除非重复任务。
属性:
下面看一下Timer的构造函数:
构造一个Timer的对象,会触发thread,那么我们看一下TimerThread干了些什么。
主要逻辑:
1、在queue上加同步器锁,当queue为空且还有线程持有timer对象,Thread wait。直到得到notify通知,这个notify是当有元素进入queue或者newTasksMayBeScheduled被置为false时会触发。
2、如果queue为空,说明没有需要处理的任务,跳出循环。
3、从queue取最近将要触发的任务,如果这个任务是CANCELLED状态,把它从queue中移除,并continue;否则,如果任务没有达到触发时间,wait(触发间隔时间)。当任务达到触发时间,如果这个任务不是重复跑的,把它从queue中移除,并把这个任务的状态置为EXECUTED。如果它是需要重复执行的,会设置它的下一次触发时间。然后执行任务的run方法执行实际的逻辑。执行完了之后,因为是一个while的无限循环,又会开始上述逻辑判断。
在上述第3条中,其实我们是依赖queue维护了一个最近触发时间的小根堆,我们看下queue的几个核心方法:
任务加入队列:
先做容量判断:数组将满时扩大为原来两倍;否则任务加入队列,注意第一个任务是放在queue[1]的位置;然后做fixUp(size);
这个方法实际是在构建一个小根堆,传入的k即为当前队列任务数量。且任务从数组下标1位置开始存放。当队列容量大于1时,先取size/2对应的元素与size对应的元素做比较,如果queue[size/2]的下一次触发时间>queue[size]的下一次触发时间,则把queue[size/2]与queue[size]对应的任务做交换。循环直到数组下标为1的元素。这样就把最近触发的任务放到了数组1的位置,最后触发的任务放到了数组末尾。
获取当前队列最近将要执行的任务
移除当前队列第一个任务
将队列最后一个任务赋给queue[1],最后一个位置设为null,任务总数减一;关键我们看一下fixDown(1)做了什么。
因为队首任务被置为了队尾的任务,则它肯定破坏了最小堆,这个方法就是要重新建立小根堆:
1、取2k对应的节点,因为k的孩子节点是2k和2k+1,
2、先判断2k和2k+1对应的值,取他们中小的,设置j的值
3、比较queue[k]与queue[j],如果执行时间不是小于关系,交换这两个任务,让下次执行时间越大的元素下移
4、循环直到队列最后位置,这样的话,执行时间离现在最久的元素被放到了队列末尾;
重复执行的任务重新设置其下次执行时间:
更新当前队首的任务的下次执行执行,同时重新调整小根堆。
目前我们还只是看了Timer的构造函数,下面我们来看Timer设置任务调度的几个方法:
schedule(TimerTasktask, long delay);
schedule(TimerTasktask, Date time);
schedule(TimerTasktask, long delay, long period);
schedule(TimerTask task, Date firstTime, long period);
以上四个方法分别表示触发一次和按频率触发多次;
scheduleAtFixedRate(TimerTasktask, long delay, long period);
scheduleAtFixedRate(TimerTasktask, Date firstTime, long period)
按固定的频率触发多次;
那么这两类方法都有按频率重复执行的逻辑,他们有什么差别呢?
我们看一下代码:
一个传的是正数一个传的是负数。而在TimerThread的mainLoop方法中,
可以看到如果period<0,下次调度时间是当前时间+间隔时间(减一个负数就是加一个正数),否则下次调度时间是下次执行时间+间隔时间,就是说他们下次触发的时间维度不一样。
以上两类方法最终都会调用
设置task的属性,将其加入到queue中,并且如果这个task是队列的第一个任务,notify阻塞在queue上面的thread。整个入队的过程持有queue的监视器锁。
下面我们看一下TimerTask,它实现了Runnable接口,其run方法由子类去实现,它有状态字段state表示任务的执行状态,包括:
默认状态是VIRGIN。
取消一个任务则调用其cancel方法
只是简单把任务状态置为CANCELLED。
总结:
Timer通过TimerThread循环触发将要执行的任务,通过TaskQueue维护一个待执行任务列表,TaskQueue内部通过一个小根堆实现待执行任务的排序。拨开底层,我们对Timer这个调度器能有一个更深的理解。