coding……
但行好事 莫问前程

Spring Boot实现定时任务

在应用开发中,经常都有用到在后台跑定时任务的需求。比如需要在服务后台跑一个定时任务来进行数据清理、数据定时增量同步、定时发送邮件、爬虫定时抓取等等,这种情况下,我们往往需要执行定时任务。在java中定时任务有多种实现方式,比如使用线程、使用Timer、使用ScheduledExecutorService、Spring Task等等。本文会简单讲述一下上述几种方式的实现方法。

1. 使用普通线程Thread

创建一个thread,然后让它在while循环里一直运行着,通过sleep方法来达到定时任务的效果。这样可以快速简单的实现,代码如下:

public class Task1 {
public static void main(String[] args) {
  // run in a second
  final long timeInterval = 1000;
  Runnable runnable = new Runnable() {
  public void run() {
    while (true) {
      // ------- code for task to run
      System.out.println("Hello !!");
      // ------- ends here
      try {
       Thread.sleep(timeInterval);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      }
    }
  };
  Thread thread = new Thread(runnable);
  thread.start();
  }
}

可以实现间隔一定时间执行任务的需求,但是功能比较单一,不能定时。这种方式比较简单明了,示例代码中就单独展示了。

2. Timer方式

Timer是JDK自带的java.util.Timer包中的工具类,通过调度java.util.TimerTask的方式让程序再某个时间按照某一个频度执行,一般用的较少。Timer调度任务的方法有两种,一个事schedule方法,另一个是scheduleAtFixedRate方法。先看一下Timer类中两个方法的重载方法:

S.N. 方法及说明
1. void schedule(TimerTask task, long delay)
将task延迟delay毫秒后执行一次
2. void schedule(TimerTask task, Date time)
将task在time启动执行一次
3. void schedule(TimerTask task, long delay, long period)
将task延迟delay毫秒后启动,之后每隔period毫秒执行一次
4. void schedule(TimerTask task, Date firstTime, long period)
将task在firstTime启动,之后每隔period毫秒执行一次,如果firstTime小于当前时间,则立即执行任务,如果某次任务执行时间大于period,本次任务执行结束,立即执行下一次任务,之后每个任务的周期还是period,整个过程不存在追任务的行为。
5. void scheduleAtFixedRate(TimerTask task, long delay, long period)
将task延迟delay毫秒后启动,之后每隔period毫秒执行一次,使用方法和3一致
6. void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
将task在firstTime启动,之后每隔period毫秒执行一次,与方法4不同的是,如果firstTime小于当前时间,则立即执行任务,之后会存在追任务的过程,比如period为5s,基本上每个任务执行时间是3s,那剩下来的两秒不再等待,直接用来执行任务,知道某个时间点,达到预期任务执行的次数。

2.1 schedule task demo

@Component
public class ScheduleTask {
    @PostConstruct
    public void startScheduleTask(){
        Timer timer = new Timer();
        //耗时2s左右的任务
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                //获得该任务该次执行的期望开始时间
                System.out.println("expect start time: " + new Date(scheduledExecutionTime()));
                System.out.println("task1 start: " + new Date());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                }
                System.out.println("task1 end: " + new Date());
            }
        };
        LocalDateTime localDateTime1 = LocalDateTime.now().plusSeconds(-11);
        Date date = Date.from(localDateTime1.atZone(ZoneId.systemDefault()).toInstant());

        System.out.println("now: " + new Date());
        System.out.println("timer start: " + date);
        //设置调度的启动时间为11s前
        timer.schedule(task1, date, 5000);
    }
}

设置任务启动时间为当前时间前的11S,每个任务执行时间为2S。

2.2 schedule task执行过程

now: Wed Sep 05 20:54:10 CST 2018
#设置任务启动时间为11S前
timer start: Wed Sep 05 20:53:59 CST 2018
#首次任务期望执行时间为当前,并不是11S前
expect start time: Wed Sep 05 20:54:10 CST 2018
task1 start: Wed Sep 05 20:54:10 CST 2018
task1 end: Wed Sep 05 20:54:12 CST 2018
#每次任务执行间隔为5S
expect start time: Wed Sep 05 20:54:15 CST 2018
task1 start: Wed Sep 05 20:54:15 CST 2018
task1 end: Wed Sep 05 20:54:17 CST 2018
expect start time: Wed Sep 05 20:54:20 CST 2018
task1 start: Wed Sep 05 20:54:20 CST 2018
task1 end: Wed Sep 05 20:54:22 CST 2018

2.3 scheduleAtFixedRate task demo

@Component
public class scheduleAtFixedRateTask {
    @PostConstruct
    public void startScheduleAtFixedRateTask() {
        Timer timer = new Timer();
        //耗时2s左右的任务
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                //获得该任务该次执行的期望开始时间
                System.out.println("expect run time: " + new Date(scheduledExecutionTime()));
                System.out.println("task1 start: " + new Date());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                }
                System.out.println("task1 end: " + new Date());
            }
        };
        LocalDateTime localDateTime1 = LocalDateTime.now().plusSeconds(-11);
        Date date = Date.from(localDateTime1.atZone(ZoneId.systemDefault()).toInstant());

        System.out.println("now: " + new Date());
        System.out.println("timer start: " + date);
        //设置调度的启动时间为11s前
        timer.scheduleAtFixedRate(task1, date, 5000);
    }
}

设置任务启动时间为当前时间前的11S,每个任务执行时间为2S。

2.4 scheduleAtFixedRate task执行过程

now: Wed Sep 05 20:59:24 CST 2018
#设置任务启动时间为11S前
timer start: Wed Sep 05 20:59:13 CST 2018
#首次任务期望启动时间为11S前
expect run time: Wed Sep 05 20:59:13 CST 2018
task1 start: Wed Sep 05 20:59:24 CST 2018
task1 end: Wed Sep 05 20:59:26 CST 2018
expect run time: Wed Sep 05 20:59:18 CST 2018
#上次任务执行结束并没有接着等3秒,直接进行下一次任务
task1 start: Wed Sep 05 20:59:26 CST 2018
task1 end: Wed Sep 05 20:59:28 CST 2018
expect run time: Wed Sep 05 20:59:23 CST 2018
task1 start: Wed Sep 05 20:59:28 CST 2018
task1 end: Wed Sep 05 20:59:30 CST 2018
expect run time: Wed Sep 05 20:59:28 CST 2018
task1 start: Wed Sep 05 20:59:30 CST 2018
task1 end: Wed Sep 05 20:59:32 CST 2018
#追赶上
expect run time: Wed Sep 05 20:59:33 CST 2018
task1 start: Wed Sep 05 20:59:33 CST 2018
task1 end: Wed Sep 05 20:59:35 CST 2018
expect run time: Wed Sep 05 20:59:38 CST 2018
task1 start: Wed Sep 05 20:59:38 CST 2018
task1 end: Wed Sep 05 20:59:40 CST 2018

注意:在示例代码中,为task类添加了@Component注解,在spring启动时,会进行Bean注册,执行@PostConstruct方法,实现了任务排期。为了展现效果,可以在测试相应task的时候,把其他的task的@Component注解注释掉。

3. ScheduledThreadPoolExecutor

Timer存在一些缺陷,比如Timer运行多个TimeTask时,只要其中有一个因任务报错没有捕获抛出的异常,其它任务便会自动终止运行。jdk5之后出现了一种新的定时器工具ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,但是ScheduledThreadPoolExecutor几个构造方法中只能设置corePoolSize。它作为一个使用corePoolSize线程和一个无界队列的固定大小的线程池,调整maximumPoolSize没有效果。一个线程负责一个schedulexxx方法的执行,corePoolSize小于执行的schedulexxx方法个数时,放入任务队列

S.N. 方法及说明
1. ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
延迟delay个unit单位执行一次command,并返回一个future,该future的get只会是null
2. <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
延迟delay个unit单位执行一次callable,并返回一个future,future可以拿到callable的结果
3. ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
延迟delay个unit单位启动command,之后每period个unit单位执行一次command
4. ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
延迟delay个unit单位启动command,每个任务之间的间隔为delay个unit单位

3.1 ScheduleWithFixedDelayTask demo

@Component
public class ScheduleWithFixedDelayTask {

    @PostConstruct
    public void starkTask(){
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                //获得该任务该次执行的期望开始时间
                System.out.println("expect start time: " + new Date(scheduledExecutionTime()));
                System.out.println("task1 start: " + new Date());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                }
                System.out.println("task1 end: " + new Date());
            }
        };

        System.out.println("now: " + new Date());
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
        executor.scheduleWithFixedDelay(task1, 0, 1000, TimeUnit.MILLISECONDS);
    }
}

3.2 ScheduleWithFixedDelayTask执行过程

now: Wed Sep 05 21:51:36 CST 2018
expect start time: Thu Jan 01 08:00:00 CST 1970
task1 start: Wed Sep 05 21:51:36 CST 2018
task1 end: Wed Sep 05 21:51:38 CST 2018
expect start time: Thu Jan 01 08:00:00 CST 1970
#下一次任务开始时间间隔了1S
task1 start: Wed Sep 05 21:51:39 CST 2018
task1 end: Wed Sep 05 21:51:41 CST 2018
expect start time: Thu Jan 01 08:00:00 CST 1970
task1 start: Wed Sep 05 21:51:42 CST 2018
task1 end: Wed Sep 05 21:51:44 CST 2018
expect start time: Thu Jan 01 08:00:00 CST 1970
task1 start: Wed Sep 05 21:51:45 CST 2018
task1 end: Wed Sep 05 21:51:47 CST 2018

另外可以发现,在ScheduleWithFixedDelayTask中,期望开始时间是不生效的。

3.3 ScheduleAtFixedRateTask demo

@Component
public class ScheduleAtFixedRateTask {
    @PostConstruct
    public void starkTask(){
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                //获得该任务该次执行的期望开始时间
                System.out.println("expect start time: " + new Date(scheduledExecutionTime()));
                System.out.println("task2 start: " + new Date());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                }
                System.out.println("task2 end: " + new Date());
            }
        };

        System.out.println("now: " + new Date());
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
        executor.scheduleAtFixedRate(task2, 0, 1000, TimeUnit.MILLISECONDS);
    }
}

3.2 ScheduleAtFixedRateTask执行过程

now: Wed Sep 05 21:54:37 CST 2018
expect start time: Thu Jan 01 08:00:00 CST 1970
task2 start: Wed Sep 05 21:54:37 CST 2018
task2 end: Wed Sep 05 21:54:39 CST 2018
expect start time: Thu Jan 01 08:00:00 CST 1970
task2 start: Wed Sep 05 21:54:39 CST 2018
task2 end: Wed Sep 05 21:54:41 CST 2018
expect start time: Thu Jan 01 08:00:00 CST 1970
task2 start: Wed Sep 05 21:54:41 CST 2018
task2 end: Wed Sep 05 21:54:43 CST 2018
expect start time: Thu Jan 01 08:00:00 CST 1970
task2 start: Wed Sep 05 21:54:43 CST 2018
task2 end: Wed Sep 05 21:54:45 CST 2018

4. Spring Task

4.1 任务定义

@Component
@Slf4j
public class SpringTask {
    @Async
    @Scheduled(cron = "0/1 * * * * *")
    public void scheduled1() throws InterruptedException {
        log.info("scheduled1 每1秒执行一次:{}", LocalDateTime.now());
        Thread.sleep(3000);
    }

    @Scheduled(fixedRate = 1000)
    public void scheduled2() throws InterruptedException {
        log.info("scheduled2 每1秒执行一次:{}", LocalDateTime.now());
        Thread.sleep(3000);
    }

    @Scheduled(fixedDelay = 3000)
    public void scheduled3() throws InterruptedException {
        log.info("scheduled3 上次执行完毕后隔3秒继续执行:{}", LocalDateTime.now());
        Thread.sleep(5000);
    }
}

这里讲解一下各个注解的含义:

@Scheduled表名方法为定时任务

  • cron:cron表达式,根据表达式循环执行,@Scheduled(cron = “0/5 * * * * *”)表示任务将在5、10、15、20…这种情况下进行工作,cron表达式可在线生成
  • fixedRate:每隔多久执行一次,@Scheduled(fixedRate = 1000) 假设第一次工作时间为2018-09-06 10:30:01,工作时长为3秒,那么下次任务的时候就是2018-09-06 10:30:04,配置成异步后,只要到了执行时间就会开辟新的线程工作;如果@Scheduled(fixedRate = 3000) 假设第一次工作时间为2018-09-06 10:30:01,工作时长为1秒,那么下次任务的时间依然是2018-09-06 10:30:04
  • fixedDelay:当前任务执行完毕后等待多久继续下次任务,@Scheduled(fixedDelay = 3000) 假设第一次任务工作时间为2018-09-06 10:30:01,工作时长为5秒,那么下次任务的时间就是2018-09-06 10:30:09
  • initialDelay:第一次执行延迟时间,只是做延迟的设定

@Async代表该任务可以进行异步工作,由原本的串行改为并行

4.2 开启线程池

@SpringBootApplication
@EnableScheduling
@EnableAsync
public class SpringTaskApplicationContext {
    public static void main(String[] args) {
        SpringApplication.run(SpringTaskApplicationContext.class, args);
    }

    /**
     *默认情况下 TaskScheduler 的 poolSize = 1 多个任务的情况下,如果第一个任务没执行完毕,后续的任务将会进入等待状态
     * @return 线程池
     */
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(10);
        return taskScheduler;
    }
}
  • @EnableScheduling: 表示开启对@Scheduled注解的解析
  • @EnableAsync: 注解表示开启@Async注解的解析,将串行化的任务给并行化。比如@Scheduled(cron = “0/1 * * * * *”)第一次工作时间为2018-09-06 10:30:01,工作周期为3秒;如果不加@Async那么下一次工作时间就是2018-09-06 10:30:04。如果加了@Async下一次工作时间就是2018-09-06 10:30:02

4.3 执行结果

scheduled2 每1秒执行一次:2018-09-06T10:23:54.510
scheduled3 上次执行完毕后隔3秒继续执行:2018-09-06T10:23:54.510
#scheduled1开启异步,每秒执行一次
scheduled1 每1秒执行一次:2018-09-06T10:23:55.014
scheduled1 每1秒执行一次:2018-09-06T10:23:56.002
scheduled1 每1秒执行一次:2018-09-06T10:23:57.001
#Schedule2定义每秒执行一次,但是任务执行时间为3S,没开启异步,所以每隔3S执行一次
scheduled2 每1秒执行一次:2018-09-06T10:23:57.513
scheduled1 每1秒执行一次:2018-09-06T10:23:58.002
scheduled1 每1秒执行一次:2018-09-06T10:23:59.002
scheduled1 每1秒执行一次:2018-09-06T10:24:00.001
scheduled2 每1秒执行一次:2018-09-06T10:24:00.513
scheduled1 每1秒执行一次:2018-09-06T10:24:01
scheduled1 每1秒执行一次:2018-09-06T10:24:02.002
#scheduled3 任务执行时间5S,任务之间间隔3S,总共8S
scheduled3 上次执行完毕后隔3秒继续执行:2018-09-06T10:24:02.513
scheduled1 每1秒执行一次:2018-09-06T10:24:03.001
scheduled2 每1秒执行一次:2018-09-06T10:24:03.513
…………

示例代码:码云 – 卓立 – 定时任务

参考链接:

  1. Java实现定时任务的三种方法
  2. java定时工具的辟谣
  3. 详解java定时任务

赞(0) 打赏
Zhuoli's Blog » Spring Boot实现定时任务
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址