前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【小家java】Java定时任务ScheduledThreadPoolExecutor详解以及与Timer、TimerTask的区别(执行指定次数停止任务)

【小家java】Java定时任务ScheduledThreadPoolExecutor详解以及与Timer、TimerTask的区别(执行指定次数停止任务)

作者头像
YourBatman
发布2019-09-03 15:01:45
3.3K0
发布2019-09-03 15:01:45
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

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

Timer和TimerTask

本文先介绍Java最原始的解决方案:Timer和TimerTask

Timer和TimerTask可以作为线程实现的第三种方式,在JDK1.3的时候推出。但是自从JDK1.5之后不再推荐时间,而是使用ScheduledThreadPoolExecutor代替

代码语言:javascript
复制
public class Timer {}
// TimerTask 是个抽象类
public abstract class TimerTask implements Runnable {}
快速入门

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

很显然,一个Timer定时器,是单线程的

代码语言:javascript
复制
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("2018/11/04 18:40:00");
        //让在指定的时刻执行(如果是过去时间会立马执行 如果是将来时间 那就等吧)
        timer.schedule(new MyTimerTask1(), time);
    }

    //被执行的任务必须继承TimerTask,并且实现run方法
    static class MyTimerTask1 extends TimerTask {
        public void run() {
            System.out.println("爆炸!!!");
        }
    }

相关API简单介绍(毕竟已经不重要了): 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)

这里需要注意区别:

  • schedule:
  • scheduleAtFixedRate: 相关文章度娘一下,可找到答案。因此本文不做介绍了,毕竟不是本文重点。
终止Timer线程

调用Timer.cancle()方法。可以在程序任何地方调用,甚至在TimerTask中的run方法中调用; 设置Timer对象为null,其会自动终止; 用System.exit方法,整个程序终止。

下面例子:

启动一个timer任务,执行指定次数/时间后停止任务

备注:该示例在某些特殊的场景会很有用的,比如守护监控、守护检查等等

代码语言:javascript
复制
/**
 * 定时器
 *
 * @author fangshixiang
 * @description //
 * @date 2019/1/22 17:55
 */
public class TaskTest {

    /**
     * 需求描述:满足条件后启动一个定时任务,再满足另外一个条件后停止此定时任务
     * (阶段性定时任务)
     * 备注:若单线程就能搞定,就使用timer即可,若需要多线程环境,请使用JDK5提供的ScheduledThreadPoolExecutor
     */
    public static void main(String[] args) {
        Timer timer = new Timer();

        // 三秒后开始执行任务,每隔2秒执行一次  当执行的总次数达到10此时,停止执行
        timer.schedule(new Task(timer, 10), 3 * 1000, 2000);
    }
}

class Task extends TimerTask {
    private Timer timer;
    private int exeCount; //此处没有线程安全问题

    public Task(Timer timer, int exeCount) {
        this.timer = timer;
        this.exeCount = exeCount;
    }

    private int i = 1;

    @Override
    public void run() {
        System.out.println("第" + i++ + "次执行任务");

        //处理业务逻辑start...
        //处理业务逻辑end...

        //若满足此条件 退出此线程
        if (i > exeCount) {
            this.timer.cancel();
            System.out.println("#### timer任务程序结束 ####");
        }
    }
}

输出:

代码语言:javascript
复制
第1次执行任务
第2次执行任务
第3次执行任务
第4次执行任务
第5次执行任务
第6次执行任务
第7次执行任务
第8次执行任务
第9次执行任务
第10次执行任务
#### timer任务程序结束 ####
Timer线程的缺点(这个就重要了)
  1. Timer线程不会捕获异常,所以TimerTask抛出的未检查的异常会终止timer线程。如果Timer线程中存在多个计划任务,其中一个计划任务抛出未检查的异常,则会引起整个Timer线程结束,从而导致其他计划任务无法得到继续执行。
  2. Timer线程时基于绝对时间(如:2014/02/14 16:06:00),因此计划任务对系统的时间的改变是敏感的。(举个例子,假如你希望任务1每个10秒执行一次,某个时刻,你将系统时间提前了6秒,那么任务1就会在4秒后执行,而不是10秒后)
  3. Timer是单线程,如果某个任务很耗时,可能会影响其他计划任务的执行。
  4. Timer执行程序是有可能延迟1、2毫秒,如果是1秒执行一次的任务,1分钟有可能延迟60毫秒,一小时延迟3600毫秒,相当于3秒(如果你的任务对时间敏感,这将会有影响) ScheduledThreadPoolExecutor的时间会更加的精确

ScheduledThreadPoolExecutor解决了上述所有问题~

ScheduledThreadPoolExecutor(JDK全新定时器调度)

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

构造方法:

代码语言:javascript
复制
ScheduledThreadPoolExecutor(int corePoolSize) //使用给定核心池大小创建一个新定定时线程池 
ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactorythreadFactory) //使用给定的初始参数创建一个新对象,可提供线程创建工厂

需要手动传入线程工厂的,可以这么弄:

代码语言:javascript
复制
    private final static ScheduledThreadPoolExecutor schedual = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
        private AtomicInteger atoInteger = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("xxx-Thread " + atoInteger.getAndIncrement());
            return t;
        }
    });
相关调度方法:

ScheduledThreadPoolExecutor还提供了非常灵活的API,用于执行任务。其任务的执行策略主要分为两大类: ①在一定延迟之后只执行一次某个任务; ②在一定延迟之后周期性的执行某个任务; 如下是其主要API:

代码语言:javascript
复制
// 执行一次
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);

// 周期性执行
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay, long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay, long period, TimeUnit unit);

第一个和第二个方法属于第一类,即在delay指定的延迟之后执行第一个参数所指定的任务,区别在于,第二个方法执行之后会有返回值,而第一个方法执行之后是没有返回值的。

第三个和第四个方法则属于第二类,即在第二个参数(initialDelay)指定的时间之后开始周期性的执行任务,执行周期间隔为第三个参数指定的时间。

但是这两个方法的区别在于:第三个方法执行任务的间隔是固定的,无论上一个任务是否执行完成(也就是前面的任务执行慢不会影响我后面的执行)。而第四个方法的执行时间间隔是不固定的,其会在周期任务的上一个任务执行完成之后才开始计时,并在指定时间间隔之后才开始执行任务。

代码语言:javascript
复制
public class ScheduledThreadPoolExecutorTest {
  private ScheduledThreadPoolExecutor executor;
  private Runnable task;
  
  @Before
  public void before() {
    executor = initExecutor();
    task = initTask();
  }
  
  private ScheduledThreadPoolExecutor initExecutor() {
    return new ScheduledThreadPoolExecutor(2);;
  }
  
  private Runnable initTask() {
    long start = System.currentTimeMillis();
    return () -> {
      print("start task: " + getPeriod(start, System.currentTimeMillis()));
      sleep(SECONDS, 10);
      print("end task: " + getPeriod(start, System.currentTimeMillis()));
    };
  }
  
  @Test
  public void testFixedTask() {
    print("start main thread");
    executor.scheduleAtFixedRate(task, 15, 30, SECONDS);
    sleep(SECONDS, 120);
    print("end main thread");
  }
  
  @Test
  public void testDelayedTask() {
    print("start main thread");
    executor.scheduleWithFixedDelay(task, 15, 30, SECONDS);
    sleep(SECONDS, 120);
    print("end main thread");
  }

  private void sleep(TimeUnit unit, long time) {
    try {
      unit.sleep(time);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  private int getPeriod(long start, long end) {
    return (int)(end - start) / 1000;
  }

  private void print(String msg) {
    System.out.println(msg);
  }
}
第一个输出:
start main thread
start task: 15
end task: 25
start task: 45
end task: 55
start task: 75
end task: 85
start task: 105
end task: 115
end main thread
第二个输出:
start main thread
start task: 15
end task: 25
start task: 55
end task: 65
start task: 95
end task: 105
end main thread

从结果,现在重点说说这两者的区别:

scheduleAtFixedRate
  • 是以上一个任务开始的时间计时period时间过去后,检测上一个任务是否执行完毕,如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行。
  • 执行周期是 initialDelay 、initialDelay+period 、initialDelay + 2 * period} 、 … 如果延迟任务的执行时间大于了 period,比如为 5s,则后面的执行会等待5s才回去执行
scheduleWithFixedDelay

是以上一个任务结束时开始计时,period时间过去后,立即执行, 由上面的运行结果可以看出,第一个任务开始和第二个任务开始的间隔时间是 第一个任务的运行时间+period(永远是这么多)

注意: 通过ScheduledExecutorService执行的周期任务,如果任务执行过程中抛出了异常,那么过ScheduledExecutorService就会停止执行任务,且也不会再周期地执行该任务了。所以你如果想保住任务都一直被周期执行,那么catch一切可能的异常。

关于ScheduledThreadPoolExecutor的使用有三点需要说明
  1. ScheduledThreadPoolExecutor继承自ThreadPoolExecutor(ThreadPoolExecutor详解),因而也有继承而来的execute()和submit()方法,但是ScheduledThreadPoolExecutor重写了这两个方法,重写的方式是直接创建两个立即执行并且只执行一次的任务;
  2. ScheduledThreadPoolExecutor使用ScheduledFutureTask封装每个需要执行的任务,而任务都是放入DelayedWorkQueue队列中的,该队列是一个使用数组实现的优先队列,在调用ScheduledFutureTask::cancel()方法时,其会根据removeOnCancel变量的设置来确认是否需要将当前任务真正的从队列中移除,而不只是标识其为已删除状态;
  3. ScheduledThreadPoolExecutor提供了一个钩子方法decorateTask(Runnable, RunnableScheduledFuture)用于对执行的任务进行装饰,该方法第一个参数是调用方传入的任务实例,第二个参数则是使用ScheduledFutureTask对用户传入任务实例进行封装之后的实例。这里需要注意的是,在ScheduledFutureTask对象中有一个heapIndex变量,该变量用于记录当前实例处于队列数组中的下标位置,该变量可以将诸如contains(),remove()等方法的时间复杂度从O(N)降低到O(logN),因而效率提升是比较高的,但是如果这里用户重写decorateTask()方法封装了队列中的任务实例,那么heapIndex的优化就不存在了,因而这里强烈建议是尽量不要重写该方法,或者重写时也还是复用ScheduledFutureTask类。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年11月04日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Timer和TimerTask
    • 快速入门
      • 终止Timer线程
        • 启动一个timer任务,执行指定次数/时间后停止任务
        • Timer线程的缺点(这个就重要了)
        • ScheduledThreadPoolExecutor(JDK全新定时器调度)
        • 相关调度方法:
          • scheduleAtFixedRate
            • scheduleWithFixedDelay
            • 关于ScheduledThreadPoolExecutor的使用有三点需要说明
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档