定时任务的执行服务

应用场景

(1) 闹钟程序或任务提醒,指定时间叫床或在指定日期提醒还信用卡。

(2) 监控系统,每隔一段时间采集下系统数据,对异常事件报警

(3) 统计系统,一般凌晨一定时间统计昨日的各种数据指标。

实现定时任务的两种方式

(1) 使用java.util包中的Timer和TimerTask。

(2) 使用Java并发包中的ScheduleExecutorService。

Timer和TimerTask

基本用法

TimerTask 表示一个定时任务,它是一个抽象类,实现了Runable,具体的定时任务需要继承该类,实现了run方法。Timer是一个具体类,它负责定时任务的调度和执行,主要方法有:

//在当前时间延时delay 毫秒后运行任务task
public void schedule(TimerTask task, long delay) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        sched(task, System.currentTimeMillis()+delay, 0);
    }

// 在指定绝对时间time 执行任务task
 public void schedule(TimerTask task, Date time) {
        sched(task, time.getTime(), 0);
    }

//固定延时重复执行,第一次计划执行时间为firstTime,后一次的计划执行时间为前一次“实际”执行时间加上period
public void schedule(TimerTask task, Date firstTime, long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), -period);
    }

// 固定延时重复执行,第一次执行时间为当前时间加上delay
 public void schedule(TimerTask task, long delay, long period) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, System.currentTimeMillis()+delay, -period);
    }

//固定频率重复执行,第一次执行时间为当前时间加上delay
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, System.currentTimeMillis()+delay, period);
    }

//固定频率重复执行,第一次计划执行时间为firstTime,后一次的计划执行时间为前一次“实际”执行时间加上period
public void scheduleAtFixedRate(TimerTask task, Date firstTime,
                                    long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), period);
    }

需要注意固定延时与固定频率的区别,二者都是重复执行,但后一次任务执行相对的时间是不一样的,对于固定延时,它是基于上次任务的“实际”执行时间来算的,如果由于某种原因,上次任务延时了,则本次任务也会延时,而固定频率会尽量补够运行次数。

另外,需要注意的是,如果第一次计划执行的时间firstTime是一个过去的时间,则任务会立即运行,对于固定延时的任务,下次任务会基于第一次执行时间计算,而对于固定频率的任务,则会从firstTime开始算,有可能加上period后还是一个过去时间,从而连续运行很多次,直到时间超过当前时间。

基本示例

package com.claa.javabasic.TimerDemo;

import java.util.Timer;
import java.util.TimerTask;

/**
 * @Author: claa
 * @Date: 2020/05/24 08:58
 * @Description:
 */
public class BasicTimer {
    static class DelayTask extends TimerTask{

        @Override
        public void run() {
            System.out.println("delayed task");
        }
    }

    public static void main(String[] args) throws InterruptedException{
       Timer timer = new Timer();

       timer.schedule(new DelayTask(),1000);
       Thread.sleep(2000);
       timer.cancel();  // 取消所有定时任务
    }
}

package com.claa.javabasic.TimerDemo;

import java.util.Timer;
import java.util.TimerTask;

/**
 * @Author: claa
 * @Date: 2020/05/24 09:20
 * @Description: 固定延时示例
 * 有两个定时任务,第一个运行一次,但耗时5秒,第二个是重复执行,1秒一次,
 * 第一个先运行,运行该程序,会发现,第二个任务只有在第一个任务运行结束后才开始运行,运行后1秒1次
 */
public class TimerFixedDelay {
    static class LongRuningTask extends TimerTask {

        @Override
        public void run() {
            try{
                Thread.sleep(5000);
            }catch(InterruptedException e){
            }
            System.out.println("long runing finished");
        }
    }

    static class FixedDelayTask extends TimerTask{

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException{
        Timer timer = new Timer();

        timer.schedule(new LongRuningTask(),10);
        timer.schedule(new FixedDelayTask(),100,1000);
    }
}

package com.claa.javabasic.TimerDemo;

import java.util.Timer;
import java.util.TimerTask;

/**
 * @Author: claa
 * @Date: 2020/05/24 09:32
 * @Description:固定频率
 * 第二个任务同样只有在第一个任务运行结束后才会运行,但它会把之前没有运行的次数补过来,一下子运行5次
 */
public class TimerFixedRate {
    static class LongRuningTask extends TimerTask {

        @Override
        public void run() {
            try{
                Thread.sleep(5000);
            }catch(InterruptedException e){
            }
            System.out.println("long runing finished");
        }
    }

    static class FixedRateTask extends TimerTask {

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException{
        Timer timer = new Timer();

        timer.schedule(new LongRuningTask(),10);
        timer.scheduleAtFixedRate(new FixedRateTask(),100,1000);
    }
}

基本原理

Timer 内部主要由任务队列和Timer线程两部分组成。任务队列是一个基于堆实现的优先级队列,按照下次执行的时间安排优先级。Timer线程负责执行所有的定时任务,需要强调的是,一个Timer对象只有一个Timer线程,所以,上面的例子,任务会被延迟。

Timer线程主体是一个循环,从队列中获取任务,如果队列中有任务且计划执行时间小于等于当前时间 ,就执行它。如果队列中没有任务或第一个任务延时还没到,就睡眠。如果睡眠过程中队列上添加了新任务且新任务是第一个任务,Timer线程会被唤醒,重新进行检查。

在执行任务之前,Timer线程判断任务是否为周期任务,如果是,就设置下次执行的时间并添加到优先级队列,对于固定延时的任务,下次执行时间为当前时间加上period, 对于固定频率的任务,下次执行时间为上次计划执行时间加上period。

需要强调的是,下次任务的计划是在执行当前任务之前就做出的,对于固定延时的任务,延时相对的是任务执行前的当前时间,而不是任务执行后,这与后面讲到的ScheduledExecutorService 的固定延时计算方法 不同,后者更合乎一般的期望。对于固定频率的任务 ,延时相对的是最先的计划,所以,很有可能会出现前面例子中一下子执行很多次任务的情况。

死循环

一个Timer对象只有一个Timer线程,这意味着,定时任务不能耗时太长,更不能是无限循环。

package com.claa.javabasic.TimerDemo;

import java.util.Timer;
import java.util.TimerTask;

/**
 * @Author: claa
 * @Date: 2020/05/24 10:16
 * @Description:Timer 循环示例
 */
public class EndLessLoopTimer {
    static class LoopTask extends TimerTask{

        @Override
        public void run() {
            while(true){
                try{
                    // 模拟执行任务
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }

    // 永远没有机会执行
    static class ExampleTask extends TimerTask{

        @Override
        public void run() {
            System.out.println("hello");
        }
    }

    public static void main(String[] args)  throws InterruptedException{
        Timer timer  = new Timer();

        timer.schedule(new LoopTask(),10);
        timer.schedule(new ExampleTask(),100);
    }
}

异常处理

关于Timer线程,在执行任何一个任务的run方法时,一旦抛出异常,Timer线程就会退出,从而所有定时任务都会被取消。

package com.claa.javabasic.TimerDemo;

import java.util.Timer;
import java.util.TimerTask;

/**
 * @Author: claa
 * @Date: 2020/05/24 10:33
 * @Description:
 */
public class TimerException {
    static class TaskA extends TimerTask {

        @Override
        public void run() {
            System.out.println("task A");
        }
    }

    static class TaskB extends TimerTask{

        @Override
        public void run() {
            try{
            System.out.println("task B");
            throw new RuntimeException();
            }catch (RuntimeException  e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();

        timer.schedule(new TaskA(),1,1000);

        timer.schedule(new TaskB(),2000,1000);
    }
}

如果希望各个定时任务不互相干扰,一定要在run方法内捕获所有异常。

注意点

(1)后台只有一个线程在运行;

(2)) 固定频率的任务被延迟后,可能会立即执行多次,将次数补够;

(3)固定延时任务的延时相对的是任务执行前的时间;

(4))不要在定时任务中使用无限循环;

(5) 一个定时任务的未处理异常会导致所有定时任务被取消。

ScheduledExecutorService

基本用法

public interface ScheduledExecutorService extends ExecutorService {

 // 单次执行,在指定延时delay 后运行command
public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

// 单次执行,在指定延时delay 后运行callable 
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);
// 固定频率重复执行                                         
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
   
// 固定延时重复执行
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);                                                  
}

对于固定频率的任务,第一次执行时间为initialDelay后,第二次为initialDelay+period,第三次为initialDelay+2*period,以此类推。不过,对于固定延时的任务,它是从任务执行后开始算的,第一次为initialDelay后,第二次为第一次任务执行结束后再加上delay

ScheduledExecutorService的主要实现类是ScheduledThreadPoolExecutor,它是线程池ThreadPoolExecutor的子类,是基于线程池实现的,它的主要构造方法是:

public ScheduledThreadPoolExecutor(int corePoolSize)

此外,还有构造方法可以接受参数ThreadFactory和RejectedExecutionHandler。

它的任务队列是一个无界的优先级队列,所以最大线程数对它没有作用,即使core-PoolSize设为0,它也会至少运行一个线程。

基本示例

由于可以有多个线程执行定时任务,一般任务就不会被某个长时间运行的任务所延迟了。

package com.claa.javabasic.ScheduledDemo;

import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @Author: claa
 * @Date: 2020/05/24 11:23
 * @Description:
 */
public class ScheduledFixedDelay {
    static class LongRuningTask extends TimerTask {

        @Override
        public void run() {
            try{
                Thread.sleep(5000);
            }catch(InterruptedException e){
            }
            System.out.println("long runing finished");
        }
    }

    static class FixedDelayTask extends TimerTask{

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService timer = Executors.newScheduledThreadPool(10);

        timer.schedule(new LongRuningTask(),10, TimeUnit.MILLISECONDS);

        timer.scheduleWithFixedDelay(new FixedDelayTask(),100,1000,TimeUnit.MILLISECONDS);
    }
}

与Timer不同,单个定时任务的异常不会再导致整个定时任务被取消,即使后台只有一个线程执行任务。

package com.claa.javabasic.ScheduledDemo;

import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @Author: claa
 * @Date: 2020/05/24 11:31
 * @Description:
 */
public class ScheduleException {
    static class TaskA extends TimerTask {

        @Override
        public void run() {
            System.out.println("task A");
        }
    }

    static class TaskB extends TimerTask{

        @Override
        public void run() {
            try{
                System.out.println("task B");
                throw new RuntimeException();
            }catch (RuntimeException  e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();

        timer.scheduleWithFixedDelay(new TaskA(),0,1, TimeUnit.SECONDS);

        timer.scheduleWithFixedDelay(new TaskB(),2,1, TimeUnit.SECONDS);

    }

}

定时任务TaskB被取消了,但TaskA不受影响,即使它们是由同一个线程执行的。不过,需要强调的是,与Timer不同,没有异常被抛出,TaskB的异常没有在任何地方体现。所以,与Timer中的任务类似,应该捕获所有异常。

基本原理

ScheduledThreadPoolExecutor的实现思路与Timer基本是类似的,都有一个基于堆的优先级队列,保存待执行的定时任务,

它的主要不同是:

(1)它的背后是线程池,可以有多个线程执行任务。

(2)它在任务执行后再设置下次执行的时间,对于固定延时的任务更为合理。

(3)任务执行线程会捕获任务执行过程中的所有异常,一个定时任务的异常不会影响其他定时任务,不过,发生异常的任务(即使是一个重复任务)不会再被调度。

参考文章

java编程的逻辑基础(马俊昌)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章