美文网首页Java 杂谈
java.util.Timer全面解析

java.util.Timer全面解析

作者: 日光倾城_700b | 来源:发表于2018-05-13 18:41 被阅读8次

    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这个调度器能有一个更深的理解。

    相关文章

      网友评论

        本文标题:java.util.Timer全面解析

        本文链接:https://www.haomeiwen.com/subject/djphrftx.html