如果你使用Java语言进行开发,对于定时执行任务这样的需求,自然而然会想到使用Timer和TimerTask完成任务,我最近就使用 Timer和TimerTask完成了一个定时执行的任务,实现得没有问题,但当在TimerTaks的run()方法中使用 Thread.sleep()方式时,可能会出现奇怪的现象,好像Timer失效了,网上查了一下,倒是有人遇到了相同的问题,但是并没有找到一篇解释为什么会出现这种情况,期待有某位达人能够分析清楚这个问题。
遇到了这样的问题,始终让我不爽,于是看了一下Timer的源码,先将了解到的内容整理如下,接下来再看看Thread.sleep()的源码,看能否找到问题所在。
在Java中,与定时任务执行相关的类很少,只有Timer、TimerTask、TimerThread、TaskQueue几个,其中每个类的职责大致如下:
Timer:一个Task的调度类,和TimerTask一样,暴露给最终用户使用的类,通过schedule方法安排Task的执行计划。该类通过TaskQueue和TimerThread类完成Task的调度。
TimerTask:实现Runnable接口,表明每一个任务均为一个独立的线程。通过run()方法提供用户定制自己任务。该类有一个比较重要的成员变量nextExecutionTime ,表示下一次执行该任务的时间。以后会看到,Timer机制就是靠这个值安排Task执行的。
TimerThread:继承于Thread,是真正执行Task的类。
TaskQueue:一个存储Task的数据结构,内部由一个最小堆实现,堆的每个成员为一个TimeTask,每个Task依靠其 nextExecutionTime值进行排序,也就是说,nextExecutionTime最小的任务在队列的最前端,从而能够现实最早执行。
要想使用Timer,用户只需要了解Timer和TimerTask,下面现已一个最基本的Timer和TimerTask使用案例入手,来看一下Timer内部的实现原理。
01 | import java.util.Timer; |
03 | import java.util.TimerTask; |
09 | class TestTimerTask extends TimerTask { |
15 | System.out.println( "TestTimerTask is running......" ); |
21 | public class TimerTaskTest { |
25 | public void testTimerTask() { |
27 | Timer timer = new Timer(); |
29 | timer.schedule( new TestTimerTask(), 0 , 10 ); |
上面的代码是一个典型的Timer&TimerTask的应用,下面先来看一下new Timer()干了什么事,其源码如下:
public Timer(String name) {
thread.setName(name); //thread为TimerThread实例。
thread.start();
}
从上面的源代码可以知道,创建Timer对象的同时也启动了TimerThread线程。下面来看看TimerThread干了什么事:
13 | newTasksMayBeScheduled = false ; |
接着来看看私有方法mainLoop()干了什么事:
01 | private void mainLoop() { |
15 | while (queue.isEmpty() && newTasksMayBeScheduled) |
27 | long currentTime, executionTime; |
29 | task = queue.getMin(); |
31 | synchronized (task.lock) { |
33 | if (task.state == TimerTask.CANCELLED) { |
41 | currentTime = System.currentTimeMillis(); |
43 | executionTime = task.nextExecutionTime; |
45 | if (taskFired = (executionTime<=currentTime)) { |
47 | if (task.period == 0 ) { |
51 | task.state = TimerTask.EXECUTED; |
57 | task.period< 0 ? currentTime - task.period |
59 | : executionTime + task.period); |
69 | queue.wait(executionTime - currentTime); |
77 | } catch (InterruptedException e) { |
也就是说,一旦创建了Timer类的实例,就一直存在一个循环在遍历queue中的任务,如果有任务的话,就通过thread去执行该任务,否则线程通过wait()方法阻塞自己,由于没有任务在队列中,就没有必要继续thread中的循环。
上面提到,如果Timer的任务队列中不包含任务时,Timer中的TimerThread线程并不会执行,接着来看看为Timer添加任务后会出现怎样的情况。为Timer添加任务就是timer.schedule()干的事,schedule()方法直接调用Timer的私有方法 sched(),sched()是真正安排Task的地方,其源代码如下:
01 | private void sched(TimerTask task, long time, long period) { |
05 | throw new IllegalArgumentException( "Illegal execution time." ); |
11 | if (!thread.newTasksMayBeScheduled) |
13 | throw new IllegalStateException( "Timer already cancelled." ); |
17 | synchronized (task.lock) { |
19 | if (task.state != TimerTask.VIRGIN) |
21 | throw new IllegalStateException( |
23 | "Task already scheduled or cancelled" ); |
29 | task.nextExecutionTime = time; |
33 | task.state = TimerTask.SCHEDULED; |
41 | if (queue.getMin() == task) |
不要奇怪,为什么要判断queue.getMin() == task时,才通过queue.notify()恢复执行。因为这种方式已经满足所有的唤醒要求了。
如果安排当前Task之前queue为空,显然上述判断为true,于是mainLoop()方法能够继续执行。
如果安排当前Task之前queue不为空,那么mainLoop()方法不会一直被阻塞,不需要notify方法调用。
调用该方法还有一个好处是,如果当前安排的Task的下一次执行时间比queue中其余Task的下一次执行时间都要小,通过notify方法可以提前打开queue.wait(executionTime - currentTime)方法对mainLoop()照成的阻塞,从而使得当前任务能够被优先执行,有点抢占的味道。
上述分析可以看出,Java中Timer机制的实现仅仅使用了JDK中的方法,通过wait & notify机制实现,其源代码也非常简单,但可以想到的是这种实现机制会对开发者造成一种困扰,sched()方法中可以看出,对于一个重复执行的任务,Timer的实现机制是先安排Task下一次执行的时间,然后再启动Task的执行,如果Task的执行时间大于下一次执行的间隔时间,可能出现不可预期的错误。当然,了解了Timer的实现原理,修改这种实现方式也就非常简单了。
java Timer 使用详解
在应用开发中,经常需要一些周期性的操作,比如每5分钟检查一下新邮件等。对于这样的操作最方便、高效的实现方式就是使用java.util.Timer工具类。比如下面的代码每5分钟检查一遍是否有新邮件:
- private java.util.Timer timer;
- timer = new Timer (true );
- timer.schedule(new java.util.TimerTask () {
- public void run() {
- //server.checkNewMail(); 检查新邮件
- }
- }, 0, 5*60*1000);
使用这几行代码之后,Timer本身会每隔5分钟调用一遍server.checkNewMail()方法,不需要自己启动线程。Timer本身也是多线程同步的,多个线程可以共用一个Timer,不需要外部的同步代码。
在《The Java Tutorial》中有更完整的例子:
- public class AnnoyingBeep {
- Toolkit toolkit;
- Timer timer;
- public AnnoyingBeep() {
- toolkit = Toolkit .getDefaultToolkit();
- timer = new Timer ();
- timer.schedule(new RemindTask(),
- 0, //initial delay
- 1*1000); //subsequent rate
- }
- class RemindTask extends TimerTask {
- int numWarningBeeps = 3;
- public void run() {
- if (numWarningBeeps > 0) {
- toolkit.beep();
- System .out.println("Beep!" );
- numWarningBeeps--;
- } else {
- toolkit.beep();
- System .out.println("Time's up!" );
- //timer.cancel(); //Not necessary because we call System.exit
- System .exit(0); //Stops the AWT thread (and everything else)
- }
- }
- }
- ...
- }
这段程序,每隔3秒响铃一声,并打印出一行消息。循环3次。程序输出如下:
Task scheduled.
Beep!
Beep! //one second after the first beep
Beep! //one second after the second beep
Time's up! //one second after the third beep
Timer类也可以方便地用来作为延迟执行,比如下面的代码延迟指定的时间(以秒为单位)执行某操作。类似电视的延迟关机功能。
- ...
- public class ReminderBeep {
- ...
- public ReminderBeep(int seconds) {
- toolkit = Toolkit .getDefaultToolkit();
- timer = new Timer ();
- timer.schedule(new RemindTask(), seconds*1000);
- }
- class RemindTask extends TimerTask {
- public void run() {
- System .out.println("Time's up!" );
- toolkit.beep();
- //timer.cancel(); //Not necessary because we call System.exit
- System .exit(0); //Stops the AWT thread (and everything else)
- }
- }
- ...
- }
===========================================================
一些注意的问题:
1. 如果希望自己的Task一直运行,而不是像上面的RemindTask那样仅循环3次。
这 很简单,Task仅代表了一次的动作。而不是让Task完成循环。上面的RemindTask中之所以有一个计数,是因为要运行3次停止,如果需要一直保 持运行。把上面有关的计数的代码全部去除即可。其实Timer中并没有提供运行指定次数后即停止的机制,所以,上面的计数满足了这个功能需要。
2. 能否设置Timer的优先级?
Timer 中是利用一个线程来进行计时及周期触发动作的。现在的Timer实现中并没有提供设置优先级的方法,在你调用new Timer()时线程已经启动,所 以,除非Timer的以后版本中在构造方法中增加优先级参数,否则,我们是不能控制其优先级的。现在的Timer默认是在 Thread.NORM_PRIORITY优先级下运行的。但是Timer提供了一个Timer(boolean isDaemon)的构造方法,可以将 Timer设置为daemon线程。这样,在你的程序中其它线程都运行完毕后,程序就可以退出了。
3. Timer是不是实现了精确地定时?
否。Timer中也是采用Object.wait(long time)来实现定时的,由于Object.wait()不保证精确计时,所以Timer也不是一个精确的时钟。如果是实时系统,不能依赖Timer。
4. 假如在一个Timer类里面再定义一个Timer类,外层定义的时间是24小时循环一次,而内层定义的是1小时循环一次,意思就是当一个小时内层运行完一次之后,内层Timer释放资源,那么外层的Timer是不是也会释放,还是24小时之中一直都在占用?
不 太清楚此占用资源是什么意思,如果在Task中用到一些资源,这些资源应该在Task运行结束后即刻释放,尤其对竞态条件,更应该占用尽量少的时间。一般 来说,一次Task的运行时间,将少于你设定的周期。否则,Task将在Timer中堆积起来。如果要实现上面说的每24小时和每1小时两个周期的操作, 可以使用一个Timer,schedule两个Task即可。
5. Timer是否可以在多线程环境下使用?
可以。Timer中用于存储Task的容器是同步了的,保证了多线程环境先的安全使用。
6. 定时的时间能为变量,可以在程序中变化吗?就是说定时的时间可以又界面来控制。
一 个Timer可以控制任意多个Task的不同周期设定的触发。也就是说,这个Timer类似于可以定多个时间的闹钟,比如我的手机就有3个闹 钟 :) 。但是一旦一个Task 已经进行了schedule了,那么就不能再次进行设定。所以,也不可能再改变其周期、延迟等等设定了。可以看下面 的代码:
- 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;
- }
7. 可不可以认为Timer就是一个线程的发射器?
Timer中仅有一个存储Task的Queue和一个调度所有Task的线程。不管你schedule几次,在Timer上加了几个Task,都只有一个后台的线程进行调度。