代码改变世界

Java定时任务Timer、TimerTask与ScheduledThreadPoolExecutor详解

2014-01-19 21:41  hduhans  阅读(6134)  评论(0编辑  收藏

   定时任务就是在指定时间执行程序,或周期性执行计划任务。Java中实现定时任务的方法有很多,本文从从JDK自带的一些方法来实现定时任务的需求。

一、Timer和TimerTask

   Timer和TimerTask可以作为线程实现的第三种方式(前两种详见《Java多线程基础》),JDK1.5之后定时任务推荐使用ScheduledThreadPoolExecutor

1、快速入门

   Timer运行在后台,可以执行任务一次,或定期执行任务。TimerTask类继承了Runnable接口,因此具备多线程的能力。一个Timer可以调度任意多个TimerTask,所有任务都存储在一个队列中顺序执行,如果需要多个TimerTask并发执行,则需要创建两个多个Timer。

  一个简单使用Timer的例子如下:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TimerTest {  
    //被执行的任务必须继承TimerTask,并且实现run方法
    static class MyTimerTask1 extends TimerTask {  
        public void run() {  
            System.out.println("爆炸!!!");  
        }  
    }     
    public static void main(String[] args) throws ParseException {  
        Timer timer = new Timer();  
        //1、设定两秒后执行任务
        //timer.scheduleAtFixedRate(new MyTimerTask1(), 2000,1000);
        //2、设定任务在执行时间执行,本例设定时间13:57:00
        SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");  
        Date time = dateFormatter.parse("2014/02/11 14:40:00");  
        timer.schedule(new MyTimerTask1(), time);
    }  
}  
View Code

2、schedule与scheduleAtFixedRate使用方法

   schedule(TimerTask task, long delay, long period)   --指定任务执行延迟时间

   schedule(TimerTask task, Date time, long period)    --指定任务执行时刻

   scheduleAtFixedRate(TimerTask task, long delay, long period)

   scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

3、schedule与scheduleAtFixedRate区别

   1) schedule:

   ① 注重任务执行的平滑度,也就是说任务队列中某个任务执行延迟了某个时间,接下来的其余任务都会延迟相同时间,来最大限度的保证任务与任务之间的时间间隔的完整性;

   ② 当程序指定开始时刻(Date time)小于当前系统时刻时,会立即执行一次任务,之后的任务开始执行时间以当前时刻为标准,结合时间间隔计算得到;

   例:计划任务程序指定从2014/02/11 18:00:00开始每隔3分钟执行一次任务。如果该程序在18:00:00之前运行,则计划任务程序分别会在18:00:00、18:03:00、18:06:00...等时间点执行任务;如果该程序在18:00:00之后运行,如在18:07:00时刻开始运行程序,计划任务程序判断指定开始执行时刻18:00:00小于当前系统时刻,于是立即执行一次任务,接下来任务时间时刻分别为18:10:00、18:13:00、18:16:00...;而当使用scheduleAtFixedRate执行计划任务时,无论计划任务程序在什么时候运行,所有任务执行的次数都按照原计划,不会因为程序执行时刻的早晚而改变。而当程序运行时刻比计划任务计划首次执行时间晚时,如同样在18:07:00时刻开始执行程序,则计划任务程序会立马计算程序执行时刻晚于指定时刻,会立即执行(18:07:00-18:00:00)/3+1=3次任务(代表18:00:00、18:03:00和18:06:00三个时刻执行的任务),接下来任务执行时刻是18:09:00、18:12:00等。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;


public class TimerRateFix {
    public static void main(String[] args) throws ParseException {
        final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");  
        Date startDate = dateFormatter.parse("2014/02/11 18:00:00");  
        Timer timer = new Timer();  
        timer.schedule(new TimerTask(){  
           public void run()  
           {  
               System.out.println("执行任务,当前时刻:" + dateFormatter.format(new Date()));  
           }  
        },startDate,3*60*1000);  
    }
}
View Code

   ③ 当执行任务的时间间隔t1大于周期间隔t2时,下一次任务执行时间点相对于上一次任务实际执行完成的时间点,每个任务的执行时间会延后,第n个计划任务的实际执行时间比预计要延后(t1-t2)*n个时间单位。

   例:计划任务程序指定从2014/02/11 18:00:00开始每隔5秒执行一次任务,每次任务执行时间为6秒。当程序在18:00:00之前执行时,schedule分别会在18:00:00、18:00:06、18:00:12...等时间点执行计划任务,每隔时间点间隔6秒。原因是根据计划,第一个计划任务应会在18:00:00执行,第二个计划任务应会在18:00:05执行,而在18:00:05时间点,第一个任务才执行了5秒,还需要1秒才执行结束,因此第二个任务不能执行,于是等待1秒后在18:00:06时刻执行,之后每个任务均如此,均比原定执行时刻有延迟,每个任务时间间隔为6秒。当使用scheduleAtFixedRate执行计划任务时,第一个计划任务在18:00:00时刻执行,第二个会根据计划在18:00:05执行,第三个会在18:00:10执行,每个任务执行时间间隔为5秒,详细执行情况如下图所示

图1 schedule与scheduleAtFixedRate任务执行区别

import java.text.ParseException;  
import java.text.SimpleDateFormat;  
import java.util.Date;  
import java.util.Timer;  
import java.util.TimerTask;  
public class TimerRateTest {  
    public static void main(String[] args) throws ParseException {  
        final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");  
        Timer timer = new Timer();  
        Date time = dateFormatter.parse("2014/02/11 18:00:00");
        //假设程序在2014/02/11 18:00:00之前启动
        //1、使用scheduleAtFixedRate,每个计划任务执行时间点严格为18:00:00、18:00:05、18:00:10...,当任务执行时间大于时间间隔时可能会有并发情况
        //2、使用schedule,每个计划任务执行时间点根据上一个任务执行结束时间及时间间隔来计算
        //     当任务执行时间t1>时间间隔t2时,第N个计划任务执行时间点延迟为(t1-t2)*N,执行时间点为18:00:00+t2*(N-1)+(t1-t2)*N
        //    当任务执行时间t1<=时间间隔t2时,第N个计划任务执行时间点无延迟,执行时间为原计划
        timer.scheduleAtFixedRate(new TimerTask(){  
           public void run() {  
               try {  
                   //每个计划任务执行时间为6秒
                   Thread.sleep(6000);  
               } catch (InterruptedException e) {  
                   e.printStackTrace();  
               }
               System.out.println("结束当前任务,当前时间:"+ dateFormatter.format(new Date()));  
           }  
        },time,5000);  //计划任务执行时间间隔为5秒
    }  
}  
View Code

   2) scheduleAtFixedRate:

   ① 注重任务执行的频度,也就是说计划任务程序开始执行,每隔任务执行的时间点就已经确定,并不会因为某个任务的延迟而延迟执行其他任务,可以保证任务执行的时间效率;

   ② 当程序指定开始时刻(Date firstTime)小于当前系统时刻时,会立即执行任务,执行次数为(当前系统时刻-指定开始时刻)/时间间隔,之后的任务开始执行时刻与当前系统时刻无关,仍按照程序指定开始时刻根据时间间隔计算得到;

   ③ 当执行任务的时间间隔t1大于周期间隔t2时,下一次任务执行时间点还是按照原定计划不变,因此这种情况,有部分时间断可能有多个任务并发执行

4、终止Timer线程

   1) 调用Timer.cancle()方法。可以在程序任何地方调用,甚至在TimerTask中的run方法中调用;

   2) 创建Timer时定义位daemon守护线程(有关守护线程见《Java守护线程》),使用new Timer(true)语句;

   3) 设置Timer对象为null,其会自动终止;

   4) 调用System.exit方法,整个程序终止。

5、Timer线程的缺点

   1) Timer线程不会捕获异常,所以TimerTask抛出的未检查的异常会终止timer线程。如果Timer线程中存在多个计划任务,其中一个计划任务抛出未检查的异常,则会引起整个Timer线程结束,从而导致其他计划任务无法得到继续执行。  

   2) Timer线程时基于绝对时间(如:2014/02/14 16:06:00),因此计划任务对系统的时间的改变是敏感的。

   3) Timer是单线程,如果某个任务很耗时,可能会影响其他计划任务的执行。

   因此,JDK1.5以上建议使用ScheduledThreadPoolExecutor来代替Timer执行计划任务。   

二、ScheduledThreadPoolExecutor

  ScheduledThreadPoolExecutor是JDK1.5以后推出的类,用于实现定时、重复执行的功能,官方文档解释要优于Timer。

1、构造方法   

   1) ScheduledThreadPoolExecutor(int corePoolSize) 使用给定核心池大小创建一个新定定时线程池 

   2) ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactorythreadFactory) 使用给定的初始参数创建一个新对象,可提供线程创建工厂

private final static ScheduledThreadPoolExecutor schedual = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
        private AtomicInteger atoInteger = new AtomicInteger(0);
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("xxx-Thread "+ atoInteger.getAndIncrement());
            return t;
        }
});
View Code

2、调度方法

   1) schedule(Callable callable, long delay, TimeUnit unit);  延迟delay时间后开始执行callable

   2) scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);  延迟initialDelay时间后开始执行command,并且按照period时间周期性重复调用,当任务执行时间大于间隔时间时,之后的任务都会延迟,此时与Timer中的schedule方法类似

   3) scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);  延迟initialDelay时间后开始执行command,并且按照period时间周期性重复调用,这里的间隔时间delay是等上一个任务完全执行完毕才开始计算,与Timer中scheduleAtFixedRate情况不同。

图2 ScheduledThreadPoolExecutor.scheduleWithFixedDelay与Timer.scheduleAtFixedRate任务执行区别

3、与Timer相比,优点有:

   1) ScheduledThreadPoolExecutor线程会捕获任务重的异常,即使多个计划任务中存在某几个计划任务为捕获异常的情况,也不会影响ScheduledThreadPoolExecutor总线程的工作,不会影响其他计划任务的继续执行。

   2) ScheduledThreadPoolExecutor是基于相对时间的,对系统时间的改变不敏感,但是如果执行某一绝对时间(如2014/02/14 17:13:06)执行任务,可能不好执行,此时可使用Timer。

   3) ScheduledThreadPoolExecutor是线程池,如任务数过多或某些任务执行时间较长,可自动分配更多的线程来执行计划任务。

   总之,JDK1.5之后,计划任务建议使用ScheduledThreadPoolExecutor。