Java Timer & TimeTask


Java 定时任务原理

在 Java 中要实现多线程有实现 Runnable 接口和扩展 Thread 类两种方式。只要将需要异步执行的任务放在 run() 方法中,在主线程中启动要执行任务的子线程就可以实现任务的异步执行。如果需要实现基于时间点触发的任务调度,就需要在子线程中循环的检查系统当前的时间跟触发条件是否一致,然后触发任务的执行。所以最原始的定时任务是创建一个 thread,然后让它在 while 循环里一直运行着,通过 sleep 方法来达到定时任务的效果。如下所示:

public class RunnableTaskDemo {

    public static void main(String[] args) {
        final long timeInterval = 2000;  

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("excute every 2 seconds, current time: " + new Date());
                    try {
                        Thread.sleep(timeInterval); //通过使线程休眠达到间隔一定时间的目的
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
    }

}

Java Timer 和 TimeTask 实现任务调度

使用Timer 和 TimeTask的简单实现如下:

public class TimerTaskDemo {

    public static void main(String[] args) {
        // TimerTask 是实现了Runnable的抽象类
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("timerTask excute every 2 seconds, current time: " + new Date());
            }
        };

        Timer timer = new Timer();
        timer.scheduleAtFixedRate(timerTask, 1000, 2000);
    }

}

可以看到,为了便于开发者快速地实现任务调度,Java JDK 对任务调度的功能进行了封装,实现了Timer 和TimerTask 两个工具类。其中TimeTask 抽象类在实现Runnable 接口的基础上增加了任务cancel() 和任务scheduledExecuttionTime() 两个方法。Timer类采用TaskQueue 来实现对多个TimeTask 的管理。TimerThread 集成自Thread 类,其mainLoop() 用来对任务进行调度。而Timer 类提供了四种重载的schedule() 方法和重载了两种sheduleAtFixedRate() 方法来实现几种基本的任务调度类型。可以发现实现原理其实和上面基本一致都是利用Java 的多线程技术,只是做了更多的封装更方便开发者使用。

详细的用法如下所示:

public class JavaTimerTaskDemo extends TimerTask{
    private String jobName = "";

    public JavaTimerTaskDemo(String jobName) { 
        super(); 
        this.jobName = jobName; 
    } 

    @Override
    public void run() {
        System.out.println("execute: " + jobName);
    }

    public static void main(String[] args) {
        // Timer 类是线程安全的,下面多个线程可以共享单个 Timer 对象而无需进行外部同步
        Timer timer = new Timer();

        long delay = 5 * 1000;
        long period = 1 * 1000;
        System.out.println("timer begin...");

        // delay 时间后执行 job1
        timer.schedule(new JavaTimerTaskDemo("job1 execute after fixed delay."), delay);

        // 指定时间执行 job2(注意大于该时间时启动任务会立即执行)
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY, 15);
        calendar.set(Calendar.MINUTE, 19);
        calendar.set(Calendar.SECOND, 00);
        Date time = calendar.getTime();
        timer.schedule(new JavaTimerTaskDemo("job2 execute at set time."), time);

        // 安排指定的任务job3在指定的时间开始进行重复的固定延迟执行(注意大于该时间时启动任务会立即执行)
        // 同理 scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
        Calendar calendar1 = Calendar.getInstance();
        calendar1.set(Calendar.HOUR_OF_DAY, 17);
        calendar1.set(Calendar.MINUTE, 59);
        calendar1.set(Calendar.SECOND, 00);
        Date time1 = calendar1.getTime();
        timer.schedule(new JavaTimerTaskDemo("job3 execute at set time."), time1, period); 
        // 等同于下面的 scheduleAtFixedRate
//        timer.scheduleAtFixedRate(new JavaTimerTaskDemo("job3 execute at set time."), time1, period);

        // 在延迟指定时间后以指定的间隔时间循环执行定时任务
        // 同理scheduleAtFixedRate(TimerTask task, long delay, long period)
        timer.schedule(new JavaTimerTaskDemo("job4 execute at fixed rate after fixed delay."), delay, period);
        // 等同于下面的 scheduleAtFixedRate
//        timer.scheduleAtFixedRate(new JavaTimerTaskDemo("job4 execute at fixed rate after fixed delay."), delay, period);
    }
}

Timer + TimeTask 定时任务的缺点

一、任务堆积

所有的TimerTask只有一个线程TimerThread来执行,因此同一时刻只有一个TimerTask在执行。一般情况下我们的线程任务执行所消耗的时间应该非常短,但是由于特殊情况导致某个定时器任务执行的时间太长,那么他就会“独占”计时器的任务执行线程,其后的所有线程都必须等待它执行完,这就会延迟后续任务的执行,使这些任务堆积在一起。

二、任务终止

任何一个TimerTask的执行异常都会导致Timer终止所有任务,在很多场景中这样的情况也是不允许的。

三、集群环境重复执行

Timer + TimeTask定任务和Spring自带的Scheduled Task(支持线程池管理)一样有一个共同的缺点,那就是应用服务器集群下会出现任务多次被调度执行的情况,因为集群的节点之间是不会共享任务信息的,每个节点上的任务都会按时执行。

四、Timer执行周期任务时依赖系统时间

Timer执行周期任务时依赖系统时间,如果当前系统时间发生变化会出现一些执行上的变化,而ScheduledExecutorService基于相对时间的延迟,不会由于系统时间的改变发生执行变化。

使用java.util.concurrent.ScheduledExecutorService代替Java.util.Timer/TimerTask

鉴于以上Timer的缺点,java.util.concurrent.ScheduledExecutorService的出现正好弥补了Timer/TimerTask的缺陷。ScheduledExecutorService基于ExecutorService,是一个完整的线程池调度。 它具有以下优点:

  • ScheduledExecutorService任务调度是基于相对时间,不管是一次性任务还是周期性任务都是相对于任务加入线程池(任务队列)的时间偏移。
  • 基于线程池的ScheduledExecutorService允许多个线程同时执行任务,这在添加多种不同调度类型的任务是非常有用的。
  • 同样基于线程池的ScheduledExecutorService在其中一个任务发生异常时会退出执行线程,但同时会有新的线程补充进来进行执行。ScheduledExecutorService可以做到不丢失任务。
public class ScheduledExecutorServiceDemo {

    public static void main(String[] args) {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ScheduledExecutorService excute every 2 seconds, current time: " + new Date());
            }
        };

        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleAtFixedRate(runnable, 1, 2, TimeUnit.SECONDS);
    }

}

总结

综上讨论会发现Timer + TimeTask实现定时任务可能真的已经成为历史,因为已经出现了更优秀更便捷的替代方式,ScheduledExecutorService拥有Timer/TimerTask的全部特性,并且使用更简单,支持并发,而且更安全,因此没有理由继续使用Timer/TimerTask,完全可以全部替换。需要说明的一点是构造ScheduledExecutorService线程池的核心线程池大小要根据任务数来定,否则可能导致资源的浪费。而Spring的Scheduled Task功能本质上就是利用java.util.concurrent.ScheduledExecutorService实现,这将在下一章进行讨论。

References

Copyright © jverson.com 2018 all right reserved,powered by GitbookFile Modify: 2018-08-25 00:16:25

results matching ""

    No results matching ""