JAVA定时任务Timer
故事起因
因业务需要,写了一个定时任务Timer,任务将在每天的凌晨2点执行,代码顺利码完,一切就绪,开始测试。运行程序,为了节省时间,将系统时间调整为第二天凌晨1点59分,看着秒针滴答滴答的转动,期盼着到2点时程序能正确运行,正暗暗欣喜之时,时间滑过2点,但是程序没有任何反应,啊哦,难道是我程序写错了。悲剧。
二次测试
首先检查自己写的程序没有什么问题。再次测试,先将时间调整为1点59分,打上断点,再运行程序,2点到来,程序运行到断点处,一步一步往下走,一切正常。为何,刚刚不是还是不能运行吗。又重复测试了几次,发现一个规律,先调整好时间后再运行程序一切正常,但是先运行程序再调整时间就什么没有任何反应。没办法了,只能研究一下JDK的Timer源码,看看内部有什么玄机。
了解内幕
我们先看看类的关系,见下图:
图1
其中Task是我自己写的任务类,这个类需要继承TimerTask,并且实现run()抽象方法,需要将任务执行的相关代码写在run方法中。
1 public class Task extends TimerTask { 2 @Override 3 public void run() { 4 System.out.println("do task..."); 5 } 6 }
执行任务的方法如下:
1 public class TimerManager { 2 public TimerManager() { 3 …… 4 5 Timer timer = new Timer(); 6 Task task = new Task(); 7 // 安排指定的任务在指定的时间开始进行重复的固定延迟执行。 8 timer.schedule(task, date, ConfigData.getDeltaTime() * 1000); 9 } 10 }
在执行任务的时候,我们只跟Timer打交道,所以先来了解一下Timer.
Timer的构造函数如下,又调用了自己的另一个构造函数 :
1 public Timer() { 2 this("Timer-" + serialNumber()); 3 } 4 5 public Timer(String name) { 6 thread.setName(name); 7 thread.start(); 8 }
到了这一步,我们需要了解thread是个什么玩意儿,我们来看看他的定义:
1 private TimerThread thread = new TimerThread(queue);
此时我们需要了解的对象成了TimerThread了,顺藤摸瓜,接着往下看吧:
1 class TimerThread extends Thread { 2 boolean newTasksMayBeScheduled = true; 3 4 private TaskQueue queue; 5 6 TimerThread(TaskQueue queue) { 7 this.queue = queue; 8 } 9 10 public void run() { 11 try { 12 mainLoop(); 13 } finally { 14 // Someone killed this Thread, behave as if Timer cancelled 15 synchronized(queue) { 16 newTasksMayBeScheduled = false; 17 queue.clear(); // Eliminate obsolete references 18 } 19 } 20 } 21 22 /** 23 * The main timer loop. (See class comment.) 24 */ 25 private void mainLoop() { 26 while (true) { 27 try { 28 TimerTask task; 29 boolean taskFired; 30 synchronized(queue) { 31 // Wait for queue to become non-empty 32 while (queue.isEmpty() && newTasksMayBeScheduled) 33 queue.wait(); 34 if (queue.isEmpty()) 35 break; // Queue is empty and will forever remain; die 36 37 // Queue nonempty; look at first evt and do the right thing 38 long currentTime, executionTime; 39 task = queue.getMin(); 40 synchronized(task.lock) { 41 if (task.state == TimerTask.CANCELLED) { 42 queue.removeMin(); 43 continue; // No action required, poll queue again 44 } 45 currentTime = System.currentTimeMillis(); 46 executionTime = task.nextExecutionTime; 47 if (taskFired = (executionTime<=currentTime)) { 48 if (task.period == 0) { // Non-repeating, remove 49 queue.removeMin(); 50 task.state = TimerTask.EXECUTED; 51 } else { // Repeating task, reschedule 52 queue.rescheduleMin( 53 task.period<0 ? currentTime - task.period 54 : executionTime + task.period); 55 } 56 } 57 } 58 if (!taskFired) // Task hasn't yet fired; wait 59 queue.wait(executionTime - currentTime); 60 } 61 if (taskFired) // Task fired; run it, holding no locks 62 task.run(); 63 } catch(InterruptedException e) { 64 } 65 } 66 } 67 }
我们可以看到TimerThread主要就是mianLoop()方法,在mainLoop方法中有这么两行代码:
1 while (queue.isEmpty() && newTasksMayBeScheduled) 2 queue.wait();
当我们 new Timer(),然后两次调用Timer()的构造函数,并调用thread.start()时,就到了mainLoop方法的这两行代码了,此时的queue.isEmpty()为true,所以线程就wait在这个地方了。什么时候将他notify呢?我们接着往下看我们自己的代码,new Timer(),new Task()之后就到了下面的这行代码了:
1 timer.schedule(task, date, ConfigData.getDeltaTime() * 1000);
继续摸索一下timer.schedule()是怎么工作的:
1 public void schedule(TimerTask task, Date firstTime, long period) { 2 if (period <= 0) 3 throw new IllegalArgumentException("Non-positive period."); 4 sched(task, firstTime.getTime(), -period); 5 } 6 7 private void sched(TimerTask task, long time, long period) { 8 if (time < 0) 9 throw new IllegalArgumentException("Illegal execution time."); 10 11 synchronized(queue) { 12 if (!thread.newTasksMayBeScheduled) 13 throw new IllegalStateException("Timer already cancelled."); 14 15 synchronized(task.lock) { 16 if (task.state != TimerTask.VIRGIN) 17 throw new IllegalStateException( 18 "Task already scheduled or cancelled"); 19 task.nextExecutionTime = time; 20 task.period = period; 21 task.state = TimerTask.SCHEDULED; 22 } 23 24 queue.add(task); 25 if (queue.getMin() == task) 26 queue.notify(); 27 } 28 }
当我们跟踪到方法sched时可以看到,这方法中设置了任务的下一次执行时间为传入的时间task.nextExecutionTime = time,然后把添加任务到队列中并notify队列。
到了这里我们要回到之前的wait位置了:
1 while (queue.isEmpty() && newTasksMayBeScheduled) 2 queue.wait();
现在queue.isEmpty()为false了,得继续往下运行,
1 currentTime = System.currentTimeMillis(); 2 executionTime = task.nextExecutionTime; 3 if (taskFired = (executionTime<=currentTime))
程序取得当前的时间和任务下一次执行的时间,比较如果执行的时间还未到则任务执行为false,即taskFired=false。
1 if (!taskFired) // Task hasn't yet fired; wait 2 queue.wait(executionTime - currentTime);
所以程序为进入wait状态,要wait多久呢?时间为executionTime – currentTime
而如果当前执行程序的时间在任务执行的时间之后了,则任务执行为true,即taskFired=true。
1 if (taskFired) // Task fired; run it, holding no locks 2 task.run();
任务立即会被执行。
豁然开朗
到这里我们就清楚了为什么之前的测试是那样一个现象,当我们先运行程序再将时间调整为1点59分后,程序一直处于queue.wait(executionTime - currentTime)状态,需要wait的时间为executionTime – currentTime,所以刚过2点时程序是不会马上运行的,必须要等待 executionTime – currentTime的时间后才能执行任务。