【Java多线程】——定时器Timer的使用

时间:2022-12-26 00:14:22

    在JDK中Timer类主要用于负责计划任务,也就是说在某个时间点开始执行一个计划好的任务。

    Timer类中的方法列表如下:

    serialNumber()
    Timer():默认构造方法;
    Timer(boolean):参数表示thread是否为守护线程
    Timer(String):参数为线程名
    Timer(String, boolean)
    schedule(TimerTask, long):参数为计划相对于当前的延迟时间
    schedule(TimerTask, Date):指定日期时间开始执行
    schedule(TimerTask, long, long):参数为延迟时间和时间间隔
    schedule(TimerTask, Date, long)
    scheduleAtFixedRate(TimerTask, long, long)
    scheduleAtFixedRate(TimerTask, Date, long)
    sched(TimerTask, long, long)
    cancel()

    purge()

    Timer类的主要作用是设置计划执行的任务,而封装任务的类是TimerTask类,类声明如下:

    public abstract class TimerTask implements Runnable{...}

    

    可以看到TimerTask是一个抽象类,实现了Runnable接口,我们需要创建子类继承TimerTask抽象类,并且将要执行的任务放入这个子类中。


一、schedule多个任务计划执行

    下面依次介绍TimerTask类中的方法

    首先先来看一下Timer的最基本用法,schedule(TimerTask, Date),我们要先创建一个类继承TimerTask类,并重写run()方法,然后实例化这个类的对象,作为schedule(TimerTask, Date)方法的参数之一。另一个参数是我们需要TimerTask中任务执行的时间。另外,当两个任务都需要计划执行,并且是使用了同一个Timer实例,而第一个执行的任务有需要耗费较长的时间时,则第二个任务会延迟进行,当第一个任务执行后第二个任务马上开始执行,两个线程不会异步执行。

public class Run1 {
	private static Timer timer = new Timer();
    static public class MyTask extends TimerTask{
		@Override
		public void run() {
			try {
				System.out.println("任务1执行时间为:"+new Date());
				Thread.sleep(5000);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
    static public class MyTask1 extends TimerTask{
    	    @Override
    	    public void run() {
    	    	    System.out.println("任务2执行时间为:"+new Date());
    	    }
    }
    
    public static void main(String args[]) {
    	    try {
    	      	MyTask mt = new MyTask();
    	      	MyTask1 mt1 = new MyTask1();
        	    SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
        	    String dateString = "2018-05-24 13:50:50";
        	    String dateString1 = "2018-05-24 13:50:53";
        	    Date dateRef = sdf.parse(dateString);
        	    Date dateRef1 = sdf.parse(dateString1);
        	    System.out.println("设置的时间:"+dateRef.toLocaleString()+" 设置的时间1:"+dateRef1.toLocaleString()+"  当前时间"+new Date().toLocaleString());
        	    timer.schedule(mt, dateRef);
        	    timer.schedule(mt1, dateRef1);
    	    }catch(ParseException e) {
    	    	    e.printStackTrace();
    	    }
    }
}

执行结果如下:

/*

设置的时间:2018-5-24 13:50:50 设置的时间1:2018-5-24 13:50:53  当前时间2018-5-24 13:49:28

任务1执行时间为:Thu May 24 13:50:50 CST 2018

任务2执行时间为:Thu May 24 13:50:55 CST 2018

*/

可以看到,两个任务之间计划执行时间相差3秒,但是第一个任务要5秒才能执行完    ,所以第二个任务在原定时间基础上推迟了两秒执行。如果需要两个任务都准时执行的话,则要为第二个任务单独创建一个Timer实例,这样两个任务就可以异步执行。

查看Timer类的jdk源码可以看到:

public Timer(String name) {

        thread.setName(name);

        thread.start();

    }

当我们实例化一个Timer的时候,实际上是将Timer类中的一个thread变量启动,这个线程会一直运行,并且在执行完相关的任务后依然继续运行下去。当我们使用Timer的构造方法Timer(boolean isDaemon)时,我们可以将这个线程设置为守护线程,这样thread就会提前结束,并且可能会在计划执行的任务还没开始执行前就结束。

ps:当设置计划时间早于当前时间时,任务立即执行。

二、其他方法介绍

    1、schedule(TimerTask task, Date firstTime, long period)

    该方法的作用是使一个指定的任务在指定日期开始之后的时间内,以参数period为时间间隔,定期地反复执行。同样的,当计划时间早于当前时间时,任务立即开始执行;当任务需要执行的时间较长,并且大于设置的时间间隔period时,任务会延期执行,也就是说时间间隔将会变长。

实例代码如下:

public class Run2 {
	private static Timer timer = new Timer();
    static public class MyTask extends TimerTask{
		@Override
		public void run() {
			try {
				System.out.println("任务1执行时间为:"+new Date());
				Thread.sleep(1000);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}    
    public static void main(String args[]) {
    	    try {
    	      	MyTask mt = new MyTask();
        	    SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
        	    String dateString = "2018-05-24 15:22:59";
        	    Date dateRef = sdf.parse(dateString);
        	    System.out.println("设置的时间:"+dateRef.toLocaleString()+"  当前时间"+new Date().toLocaleString());
        	    timer.schedule(mt, dateRef, 2000);
    	    }catch(ParseException e) {
    	    	    e.printStackTrace();
    	    }
    }
}

运行结果:

/*

设置的时间:2018-5-24 15:22:59  当前时间2018-5-24 15:22:18

任务1执行时间为:Thu May 24 15:22:59 CST 2018

任务1执行时间为:Thu May 24 15:23:01 CST 2018

任务1执行时间为:Thu May 24 15:23:03 CST 2018

任务1执行时间为:Thu May 24 15:23:05 CST 2018

*/

可以看到,指定的TimerTask任务每隔两秒执行一次,然后我们将代码中的Thread.sleep(1000)改为Thread.sleep(5000),运行结果如下:

/*

设置的时间:2018-5-24 15:20:59  当前时间2018-5-24 15:20:59

任务1执行时间为:Thu May 24 15:20:59 CST 2018

任务1执行时间为:Thu May 24 15:21:04 CST 2018

任务1执行时间为:Thu May 24 15:21:09 CST 2018

任务1执行时间为:Thu May 24 15:21:14 CST 2018

*/

任务的执行出现了延迟,时间间隔变为了5秒。

    2、TimerTask.cancel()和Timer.cancel()

    TimerTask.cancel是用来将本任务从Timer的执行队列中删除掉,使之不再继续执行;Timer.cancel()是将本Timer的所有任务都撤销,不再继续执行任何任务,同时进程会被销毁,程序结束。但是有时调用cancel()方法的线程可能没有第一时间竞争到queue的对象锁,那么任务可能会完成执行,没有被及时销毁。

    3、scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

    scheduleAtFixedRate和schedule一样,都可以按规定时间按顺序执行规定的计划,并且不需要考虑线程安全的问题,他们之间的区别在于两者在面对多任务不延时的情况,处理结果会有区别。首先说明延时的情况,再多任务情况下,如果由于上一个任务执行时间过长而导致下一个任务延时,那么上一个任务执行完毕后下一个任务立即执行,两个方法的处理方式都一样,这时所设置的延时period看起来就没什么作用了,因为任务总是一个接一个的执行。

    但是当period大于任务执行时间的时候,要注意的一点是,如果设置的开始执行时间早于实际运行的时间,两种方法都会将前面没有完成的工作补上,但是补上工作的方法有区别,这就涉及到两者之间的差异——追赶执行性。scheduleAtFixedRate是具备追赶执行性的,而schedule不具备这个性质。

    我们用一个例子来解释一下什么叫做追赶执行性,假设有两个要工厂生产零件,生产任务相同,都是每天需要生产100个零件,生产完就停下来不继续生产,一共生产10天。当生产到第5天的时候,突然两个工厂所在的地区停电3天,没法继续生产。到了第8天来电之后又恢复生产,这个时候两个工厂会采取不同的生产方案。

    工厂A采用了schedule方案,第8天开始继续生产第5天本应该生产的100零件,并且生产够100后停下,等到第9天再继续开始生产。这样由于停电3天,工厂A一共用了13天完成了生产任务。

    工厂B的领导为了弥补停电带来的生产延误,决定采用scheduleAtFixedRate方案,即从第8天开始,不间断地加班加点生产零件,直到将因为停电落下的300零件生产完毕,再恢复之前的生产计划每天100零件。结果从第8天开始工厂B用了一天不间断的生产终于补齐了300个零件,从第9天开始,恢复每天生产100个零件(注意:第9天生产的零件是原计划第8天应该生产的)。最终工厂B用了11天就完成了生产任务。

public class Run3 {
	private static Timer timer = new Timer();
    static public class MyTask extends TimerTask{
		@Override
		public void run() {
			System.out.println("任务执行时间为:"+new Date());
			System.out.println("结束时间为:"+new Date());
		}
	}    
    public static void main(String args[]) {
    	    try {
    	      	MyTask mt = new MyTask();
        	    SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
        	    String dateString = "2018-05-24 19:29:10";
        	    Date dateRef = sdf.parse(dateString);
        	    System.out.println("设置的时间:"+dateRef.toLocaleString()+"  当前时间"+new Date().toLocaleString());
        	    timer.schedule(mt, dateRef, 3000);
    	    }catch(ParseException e) {
    	    	    e.printStackTrace();
    	    }
    }
}

运行结果如下:

/*

设置的时间:2018-5-24 19:29:10  当前时间2018-5-24 19:49:48

任务执行时间为:Thu May 24 19:49:48 CST 2018

结束时间为:Thu May 24 19:49:48 CST 2018

任务执行时间为:Thu May 24 19:49:51 CST 2018

结束时间为:Thu May 24 19:49:51 CST 2018

任务执行时间为:Thu May 24 19:49:54 CST 2018

结束时间为:Thu May 24 19:49:54 CST 2018

任务执行时间为:Thu May 24 19:49:57 CST 2018

结束时间为:Thu May 24 19:49:57 CST 2018

*/

我们可以看到,虽然执行的时间比原计划延迟了,但是由于schedule不具备追赶执行性,任务依旧是按照我们设置的时间间隔每三秒执行一次。但是如果我们将schedule改成scheduleAtFixedRate之后,情况就不一样了:

/*

任务执行时间为:Thu May 24 19:29:58 CST 2018

结束时间为:Thu May 24 19:29:58 CST 2018

任务执行时间为:Thu May 24 19:29:58 CST 2018

结束时间为:Thu May 24 19:29:58 CST 2018

任务执行时间为:Thu May 24 19:29:58 CST 2018

结束时间为:Thu May 24 19:29:58 CST 2018

任务执行时间为:Thu May 24 19:29:58 CST 2018

结束时间为:Thu May 24 19:29:58 CST 2018

任务执行时间为:Thu May 24 19:30:01 CST 2018

结束时间为:Thu May 24 19:30:01 CST 2018

任务执行时间为:Thu May 24 19:30:04 CST 2018

结束时间为:Thu May 24 19:30:04 CST 2018

任务执行时间为:Thu May 24 19:30:07 CST 2018

结束时间为:Thu May 24 19:30:07 CST 2018

*/

可以看到,红色区域是“追赶执行的”任务,它们会连续执行而不管时间间隔period,直到将落下的任务执行完毕,这就算追赶上了原来计划的任务,从这里开始按照设定的时间间隔执行接下来的任务。这就是scheduleAtFixedRate所具备的追赶执行性。