Timer与TimerTask的使用

java.util 套件之中有两个类别与工作排程非常有关系,他们分别是Timer与TimerTask。其中,Timer 是一个定时器,可以设定成特定时间或特定时间周期产生讯号。不过,只有Timer 是没有用的,必须配合TimerTask 才有作用。Timer一旦与某个TimerTask 产生关联,就会在产生讯号的同时,连带执行TimerTask所定义的工作。

TimerTask 的制作非常容易,任何一个类别只要继承TimerTask 类别,并实作其run()方法即可。Run()方法就是我们自行定义的工作,一旦Timer 在特定时间或特定时间周期产生讯号,run()方法就会被执行。我们会透过Timer 的schedule()方法来设定特定时间或特定时间周期,并将它与某个TimerTask 联系(进行TimerTask 的排程)。最后,您可以使用Timer 的cancel()方法来停止Timer,叫用cancel()之后,Timer 就会和TimerTask 脱离关系。TimerTask 本身也有cancel()方法。

假设我们希望在3 秒之后执行某些工作,范例程序如下:

//MyTask1.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class MyTask1 extends TimerTask {
    public void run() {
        //印出run()被叫用的时间
        System.out.println("Task1 Fire Time:");
        System.out.println(System.currentTimeMillis());
    }
}


//TimerTest1.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class TimerTest1 extends MIDlet {
    public TimerTest1() {
    }

    public void startApp() {
        Timer timer = new Timer();
        //规定3000 毫秒之后(3 秒)启动MyTask1
        timer.schedule(new MyTask1(), 3000);
        //印出排程后的时间
        System.out.println("Task Schedule Time:");
        System.out.println(System.currentTimeMillis());
    }

    public void pauseApp() {
    }

    public void destroyApp(boolean unconditional) {
    }
}

执行结果:
Task Schedule Time:
1036932682671
Task1 Fire Time:
1036932685671

执行时,屏幕上会先印出
Task Schedule Time:
1036932682671

大约三秒钟之后才会印出。
Task1 Fire Time:
1036932685671

我们可以发现两者的时间差距为3000 豪秒。

Timer 的schedule()方法除了可以给定一个long 型态,也可以给定一个Date类别的实体。

几舍我们希望从现在起5 秒之后执行某些工作,范例程序如下:

//MyTask2.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class MyTask2 extends TimerTask {
    public void run() {
        System.out.println("Task2 Fire Time:");
        System.out.println(System.currentTimeMillis());
    }
}


//TimerTest2.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class TimerTest2 extends MIDlet {
    public TimerTest2() {
    }

    public void startApp() {
        Timer timer = new Timer();
        Date after5sec = new Date(System.currentTimeMillis() + 5000);
        timer.schedule(new MyTask2(), after5sec);
        System.out.println("Task Schedule Time:");
        System.out.println(System.currentTimeMillis());
    }

    public void pauseApp() {
    }

    public void destroyApp(boolean unconditional) {
    }
}

执行结果:
Task Schedule Time:
1036933242234
Task2 Fire Time:
1036933247250

执行时,屏幕上会先印出
Task Schedule Time:
1036933242234

大约五秒钟之后才会印出。
Task2 Fire Time:
1036933247250

我们可以发现两者的时间差距大约为5000 豪秒。之所以无法精确定差5000 毫秒,是因为在排程(呼叫timer.schedule(new MyTask2(),after5sec))之前,系统必须先取得目前时间,再产生Date 对象,这些动作都会造成些微的时间差。

上述两个范例都是设定在某个特定时间产生讯号,接下来我们将示范如何在某个特定的时间周期产生讯号。

假设我们希望在离现在5 秒钟之后,每格4 秒中产生讯号,范例程序如下:

//MyTask3.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class MyTask3 extends TimerTask {
    long lasttime;
    public void run() {
        long now = System.currentTimeMillis();
        System.out.print("Task3 Fire Time: ");
        System.out.print(now);
        System.out.print(" ; Intervel :");
        //计算出与上一次的时间差
        System.out.println(now - lasttime);
        //重设上次发生时间为现在
        lasttime = now;
    }
}


//TimerTest3.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class TimerTest3 extends MIDlet {
    Timer timer;
    public TimerTest3() {
    }

    public void startApp() {
        Form f = new Form("Test");
        Display.getDisplay(this).setCurrent(f);
        timer = new Timer();
        //Date after5sec = new Date(System.currentTimeMillis()+5000) ;
        MyTask3 mt = new MyTask3();
        mt.lasttime = System.currentTimeMillis();
        //timer.schedule(mt,after5sec,4000);
        timer.schedule(mt, 5000, 4000);
        System.out.println("Task Schedule Time:");
        System.out.println(System.currentTimeMillis());
    }

    public void pauseApp() {
    }

    public void destroyApp(boolean unconditional) {
        timer.cancel();
    }
}

执行结果:
Task Schedule Time:
1036934383453
Task3 Fire Time: 1036934388453 ; Intervel :5000
Task3 Fire Time: 1036934392453 ; Intervel :4000
Task3 Fire Time: 1036934396453 ; Intervel :4000
Task3 Fire Time: 1036934400453 ; Intervel :4000
Task3 Fire Time: 1036934404453 ; Intervel :4000
….

从范例中可以看出,第一个讯号出现在5 秒之后,往后的讯号都是每隔4 秒产生一次。周期性产生讯号所使用的sechdule()方法,其第二个参数可以是long或Date。在此范例中,使用了:
Form f = new Form("Test") ;
Display.getDisplay(this).setCurrent(f) ;
设定MIDlet 的显示画面,这纯粹是为了避开J2ME WTK 仿真器本身的bug,没有任何特别意义。

在MyTask3.java 之中,我们使用
long now = System.currentTimeMillis();
来抓取run()一开始执行时的时间。如果任何时候想到知道这一次的run()在何时启动,我们可以随时叫用TimerTask 的scheduledExecutionTime()方法。所以上述取得开始时间的方式可以用
long now = scheduledExecutionTime() ;
来取代,所以MyTask3.java 可以改成下面方式实作:

//MyTask3.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class MyTask3 extends TimerTask {
    long lasttime;
    public void run() {
        System.out.print("Task3 Fire Time: ");
        long now = scheduledExecutionTime();
        System.out.print(now);
        System.out.print(" ; Intervel :");
        System.out.println(now - lasttime);
        lasttime = now;
    }
}

由于scheduledExecutionTime()方法只传回这次run()的起始时间,所以在任何地方呼叫都不会影响到时间精确度。更重要的一点, 如果scheduledExecutionTime()和System.currentTimeMillis()都可以达到我们的目的时,使用scheduledExecutionTime()比较不会浪费系统资源。另外要注意的是,当您不希望Timer 再产生讯号,您可以使用Timer 的cancel()方法来停止Timer,叫用cancel()之后,Timer 就会和TimerTask 脱离关系,而且不再产生任何讯号,此时TimerTask 才能重新被排程。

Timer 属于系统资源,每次MIDlet 离开之前,都应该使用Timer 的cancel()方法来停止Timer。TimerTask 本身也有cancel()方法,如果TimerTask 被排定特定的时间周期执行,那么叫用TimerTask 的cancel()方法之后,也可以保证往后其工作都不会再被执行。

最后,我们要讨论的是讯号精确度的问题。前面的范例中,TimerTask 里头都只有做简单的计算。由于简单的计算花的时间不多,所以都可以在下次重新产生讯号前完成。可是,如果run()里头的工作无法在下次产生讯号前完成呢?举例来说,我们希望每4 秒钟产生一次讯号,但是run()方法有时确需要5.5秒钟完成工作,有时2.2 秒就完成,范例如下:

//MyTask4.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class MyTask4 extends TimerTask {
    long lasttime;
    public void run() {
        long now = System.currentTimeMillis();
        System.out.print("Task4 Fire Time: ");
        System.out.print(now);
        System.out.print(" ; Intervel :");
        System.out.println(now - lasttime);
        lasttime = now;
        doSomething();
    }

    public void doSomething() {
        //用随机数来模拟工作所需时间,有时超过4000 豪秒,有时少于4000 豪秒
        try {
            Random rdm = new Random();
            int i = rdm.nextInt() % 2;
            if (i == 0) {
                Thread.sleep(2500);
                System.out.println("doSomething : 2500 millisecond");
            } else {
                Thread.sleep(5500);
                System.out.println("doSomething : 5500 millisecond");
            }
        } catch (Exception e) {}
    }
}


//TimerTest4.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class TimerTest4 extends MIDlet {
    Timer timer;
    public TimerTest4() {
    }

    public void startApp() {
        Form f = new Form("Test");
        Display.getDisplay(this).setCurrent(f);
        timer = new Timer();
        //Date after5sec = new Date(System.currentTimeMillis()+5000) ;
        MyTask4 mt = new MyTask4();
        mt.lasttime = System.currentTimeMillis();
        //timer.schedule(mt,after5sec,4000);
        timer.schedule(mt, 5000, 4000);
        System.out.println("Task Schedule Time:");
        System.out.println(System.currentTimeMillis());
    }

    public void pauseApp() {
    }

    public void destroyApp(boolean unconditional) {
        timer.cancel();
    }
}

执行结果:

Task Schedule Time:
1036936893265
Task4 Fire Time: 1036936898265 ; Intervel :5000
doSomething : 5500 millisecond
Task4 Fire Time: 1036936903765 ; Intervel :5500
doSomething : 2500 millisecond
Task4 Fire Time: 1036936907765 ; Intervel :4000
doSomething : 2500 millisecond
Task4 Fire Time: 1036936911765 ; Intervel :4000
doSomething : 5500 millisecond
Task4 Fire Time: 1036936917265 ; Intervel :5500
doSomething : 5500 millisecond
Task4 Fire Time: 1036936922765 ; Intervel :5500
doSomething : 2500 millisecond
Task4 Fire Time: 1036936926765 ; Intervel :4000
doSomething : 5500 millisecond

从这个范例可以看出schedule()方法的运作属于一种称为fixed-delay execution 的模式。意思就是说,如果run()在时间周期之内完成,那么下一次的讯号就会在下一次预定的时间产生。但是,如果run()无法在时间周期之内完成,那么下一次的讯号就延迟,而且上一个run 执行之后就会立刻产生讯号。所以会有上述的结果。另外一个可以在固定周期产生讯号的方法为scheduleAtFixedRate(),但是其特性和schedule()不太相同。。范例如下(TimerTask 的部分我们仍采用MyTask4)

//TimerTest5.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;

public class TimerTest5 extends MIDlet {
    Timer timer;
    public TimerTest5() {
    }

    public void startApp() {
        Form f = new Form("Test");
        Display.getDisplay(this).setCurrent(f);
        timer = new Timer();
        //Date after5sec = new Date(System.currentTimeMillis()+5000) ;
        MyTask4 mt = new MyTask4();
        mt.lasttime = System.currentTimeMillis();
        //timer.scheduleAtFixedRate(mt,after5sec,4000);
        timer.scheduleAtFixedRate(mt, 5000, 4000);
        System.out.println("Task Schedule Time:");
        System.out.println(System.currentTimeMillis());
    }

    public void pauseApp() {
    }

    public void destroyApp(boolean unconditional) {
        timer.cancel();
    }
}

执行结果:
Task Schedule Time:
1036937109687
Task4 Fire Time: 1036937114703 ; Intervel :5016
doSomething : 5500 millisecond
Task4 Fire Time: 1036937120203 ; Intervel :5500
doSomething : 5500 millisecond
Task4 Fire Time: 1036937125703 ; Intervel :5500
doSomething : 2500 millisecond
Task4 Fire Time: 1036937128203 ; Intervel :2500
doSomething : 2500 millisecond
Task4 Fire Time: 1036937130703 ; Intervel :2500
doSomething : 2500 millisecond
Task4 Fire Time: 1036937134703 ; Intervel :4000
doSomething : 5500 millisecond
Task4 Fire Time: 1036937140203 ; Intervel :5500
doSomething : 2500 millisecond
Task4 Fire Time: 1036937142703 ; Intervel :2500
doSomething : 2500 millisecond
Task4 Fire Time: 1036937146703 ; Intervel :4000
doSomething : 5500 millisecond

从这个范例可以看出scheduleAtFixedRate()方法的运作属于一种称为fixed-rate execution 的模式。意思就是说,如果run()在时间周期之内完成,那么下一次的讯号就会在下一次预定的时间产生。但是,如果run()无法在时间周期之内完成,那么等到run 完成之后,下一次的讯号产生就尽量赶上原本应该产生讯号的时间。以上面的例子来说,第二次的讯号应该在第一次产生讯号之后4000 毫秒之后产生,可是却因为上一次的执行使得必需延迟1500 毫秒。

同样的,第三次讯号的产生应该在第一次产生讯号之后8000 毫秒之后产生,可是由于第二次的run()延迟1500 毫秒之后才完成,所以总计就比预计时间延迟了3000 毫秒。第三次所需工作时间只要2500 毫秒,因此完成第三次run()之后,系统必须要尽快赶上原本预计的时间,所以立刻产生讯号,使得第四次与第三次之间的时间差只有2500 毫秒,此时就赶上了1500 毫秒。第四次所需工作时间只要2500 毫秒,因此完成第四次run()之后,系统必须要尽快赶上原本预计的时间,所以立刻产生讯号,使得第五次与第四次之间的时间差只有2500 毫秒,此时又赶上了1500 毫秒。到目前为止,已经把落后都追回来了,这就是尽量赶上的意思。所以即使第五次的工作只花了2500 毫秒就完成,但是由于没有落后的进度需要追赶,所以和第六次的时间相差就是预计的4000 毫秒