作者:你我她啊 | 来源:互联网 | 2023-09-13 13:55
java.util.Timer简介Timer是用于管理在后台执行的延迟任务或周期性任务,其中的任务使用java.util.TimerTask表示。任务的执行方式有两种
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() {@Overridepublic void run() {System.out.println("haha");try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}}}, 1000, 1000);timer.schedule(new TimerTask() {@Overridepublic 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 cancelledsynchronized(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 thinglong 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为trueif (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&#39;t yet fired; waitqueue.wait(executionTime - currentTime);}//如果下次执行的时间早于当前时间,则说明达到了执行的时间点,可以执行任务if (taskFired) // Task fired; run it, holding no lockstask.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 leakfixDown(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