Java并发学习之定时任务的几种玩法

Java中创建和玩转定时任务

定时任务,在日常工作中,可以说是一个算是一个常见的需求场景,比如定时数据校验,数据报表输出,报警等

0. 前言

前面一篇博文《Java并发学习之四种线程创建方式的实现与对比》, 有朋友指出线程池的方式应该算不上新的方式,而应该把Timer方式创建线程加上

这个却是我个人见识不够,写的时候没有想到Timer这种场景了,所以说分享学习记录,不仅仅可以帮助别人,自己也会因此收益

感谢@超大小龙虾 的指正,同时欢迎各位大侠对小弟多多指教

I. 定时任务创建的几种方式

这里给出几种个人接触过的定时任务使用方式(不全,仅供大家参考)

  1. 最简单的一种:在线程中执行 Thread.sleep(),休眠挂起线程,等待一段时间后再执行
  2. 借助Timer和TimerTask实现定时任务
  3. 借助调度线程池 Executors.newScheduledThreadPool() 实现定时任务
  4. 借助第三方工具,如spring的定时任务; Quartz(听过没用过);以及其他一些开源工具或公司内的服务

下面简单介绍上面的几种思路,以及一般的使用姿势

1. Thread#sleep方式

严格来讲,这种不太能够算入定时任务的范畴,为什么这么说?

一般我们所说的定时任务可以区分为两种,一种是到了某个点自动执行;另一种就是每隔多长时间执行一次

而这种线程Sleep的方式,则是在运行后,强制使线程进入阻塞状态一段时间,然后再执行后续的逻辑,一般的使用流程是

// 提前的业务逻辑 xxx
try {
  Thread.sleep(1000); // 睡眠1s
} catch(Exception e) {
  // ....
}
// 定时任务的业务逻辑
// xxx

这里把这个也放在定时任务里,可以看下面结合实例的case中的演示,利用这个sleep也可以非常猥琐的实现定时需求

2. Timer & TimerTask方式

TimerTask 是一个实现 Runnable的抽象类,因此可以将需要定时处理的业务逻辑封装在这个Task里面;然后通过Timer封装类来定时调度

TimerTask的使用姿势和一般的Runnable接口没啥两样

一般使用姿势如下

// 创建timer实例
Timer timer = new Timer("demo);

// 撰写定时任务逻辑
TimerTask task = new TimerTask() {
    @Override
    public void run() {
        System.out.println("timerTask: " + System.currentTimeMillis());
    }
};

// 定时调度执行
// 1. 100ms后开始执行task任务
timer.schedule(task, 100); 

// 2. 100ms后,首次执行,并且每隔100ms执行一次task任务
timer.scheduleAtFixedRate(task, 100, 100);

这个就有意思一点了,可以支持定时执行,也可以支持按一个频率执行,且一般使用可以将上面的步骤进行缩减, 直接这么玩

new Timer("schedule").schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("timerTask: " + System.currentTimeMillis());
    }
}, 100);

3. Executors#newScheduledThreadPool线程池方式

Executors提供了一批创建线程池的方式,除了常见的创建固定大小的线程池之外,还有个一就是创建ScheduledExecutorService来实现定时任务调度

借助Executors#newScheduledThreadPool来实现定时任务非常简单

// 获取线程池
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);


// 延迟100ms后,执行定时任务
executorService.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("task: " + System.currentTimeMillis());
    }
}, 100, TimeUnit.MILLISECONDS);


// 100ms后,首次执行,然后每个100ms执行一次
executorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("task: " + System.currentTimeMillis());
    }
}, 100, 100, TimeUnit.MILLISECONDS)

从使用姿势来看,和Timer方式差不离,同样支持定时执行与每隔多长时间执行两种方式

4. spring的定时任务

spring方式就非常强大了,而且支持注解的配置方式,配置完毕,然后在方法上加一个注解,就可以实现定时执行了

常见的使用姿势

// 100ms后执行
@Scheduled(fixedDelay = 100)
public void doSomething() { 
    // something that should execute periodically
}


// 每隔100ms执行一次
@Scheduled(fixedRate = 100)
public void doSomething() { 
    // something that should execute periodically
}

而且比较厉害的是,这个还支持cron表达式

II. 结合实例演示四种定时任务使用姿势

来两个实际的应用场景,用上面的四种方式分别实现

case:

系统中有一些统计数据,需要离线计算,每天凌晨计算完之后导入,然后需要一个定时任务,假设凌晨5点数据导入完毕;在5:15分进行校验,判断数据是否正常导入;校验完成之后,45分钟后即六点,将校验结果通知给owner

1. Thread#sleep 实现方式

采用sleep的方式实现定时任务,因为其本身不支持定时的情况,所以就只能比较猥琐的计算需要sleep的时间了

实现代码如下(非精确的实现方式,主要为了演示如何用sleep来实现上面这种场景)

public class SleepDemo {
  static class Task implements Runnable {
      public void run() {
          while (true) {
              Calendar calendar = Calendar.getInstance();
              int hour = calendar.get(Calendar.HOUR_OF_DAY);
              int min = calendar.get(Calendar.MINUTE);
              int sleepHour, sleepMin;

              // 计算sleep的小时数; 若在启动时,在五点前 5-hour; 否则需要加一天
              sleepHour = 5 - hour < 0 ? (24 + 5 - hour) : (5 - hour);
              sleepMin = 15 - min; // 计算sleep的分钟数
              try {
                  long sleepTime = ((sleepHour * 60) + sleepMin) * 60 * 1000L;
                  Thread.sleep(sleepTime);

                  // 开始校验数据是否存在
                  System.out.println("数据校验");

                  // 等待到6点,开始报警
                  int second = calendar.get(Calendar.SECOND);
                  sleepTime = ((59 - calendar.get(Calendar.MINUTE)) * 60 + 60 - second) * 1000L;
                  Thread.sleep(sleepTime);
                  
                  System.out.println("开始报警");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  }

  public static void main(String[] args) {
      new Thread(new Task(),"sleepDemo").start();
  }
}

简单说明下上面的实现思路:

  • 采用while(true)死循环,来实现每隔多长时间来执行一次
  • 对于定时触发任务的场景,需要计算指定时间与当前时间的差值,作为sleep的时间

这个实现方式,虽说可以完成目标,但是非常的不优雅,下面来看下Timer的实现方式

2. Timer&TimerTask 实现方式

使用Timer,需要借助TimerTask类,在其中书写定时任务的逻辑,因为case中有一个每隔一天跑一次的定时任务和一个延迟任务,所以这里用到了Timer的两种定时任务使用方式

public class TimerDemo {
    static class Task extends TimerTask {
        @Override
        public void run() {
            System.out.println("开始执行任务");

            // 执行完毕,等待到6点发送报警
            int min = Calendar.getInstance().get(Calendar.MINUTE);
            int sec = Calendar.getInstance().get(Calendar.SECOND);
            long delayTime = ((59 - min) * 60 + 60 - sec) * 1000L;
            new Timer().schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("报警");
                }
            }, delayTime);
        }
    }

    public static void main(String[] args) {
        Date date = new Date();
        if (date.getHours() == 5 && date.getMinutes() > 15 || date.getHours() > 5) {
            date.setHours(5);
        } else {
            date.setMinutes(15);
        }
        date.setSeconds(0);

        new Timer().scheduleAtFixedRate(new Task(), date, 24 * 2600 * 1000L);
    }
}

相比与上一个,稍微好了那么一丢丢,至少从代码结构上来看简洁了很多

3. Executors.newScheduledThreadPool的实现方式

定时任务的方式,用起来和前面差不多,依然是两种方式的混搭

public class ScheduleDemo {

    static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);

    static class Task extends TimerTask {
        @Override
        public void run() {
            System.out.println("开始执行任务");

            // 执行完毕,等待到6点发送报警
            int min = Calendar.getInstance().get(Calendar.MINUTE);
            int sec = Calendar.getInstance().get(Calendar.SECOND);
            long delayTime = (59 - min) * 60 + 60 - sec;
            executorService.schedule(() -> System.out.println("报警"), delayTime, TimeUnit.SECONDS);
        }
    }

    public static void main(String[] args) {
        Calendar calendar = Calendar.getInstance();
        // 计算sleep的小时数
        int sleepHour = 5 - calendar.get(Calendar.HOUR_OF_DAY);
        if(sleepHour < 0) { // 算下一天
            sleepHour = 24 + sleepHour;
        }

        // 计算sleep的分钟数
        int sleepMin = 15 - calendar.get(Calendar.MINUTE);
        long sleepTime = ((sleepHour * 60) + sleepMin) * 60 * 1000L;
        
        executorService.scheduleAtFixedRate(new Task(), sleepTime, 24 * 3600, TimeUnit.SECONDS);
    }
}

与Timer在用法上不同的一个是这里可以指定延迟的时间单位;但是希望在指定的时间进行执行时,依然还是得计算初始的延迟时间,和sleep使用方式中差不多

上面三中,是jdk本身就支持的定时任务的支持;总得来说,能实现你的需求场景,但是不好用,还得让自己去计算delayTime/sleepTime;讲道理,这对使用者而言,实在是不能更不友好了;

但是在另一方面,若延迟时间比较容易确认的话;或者单纯的使用每隔多长时间调度一次的话,TimerScheduledExecutorService两种方式都还不错

  • Timer 在指定时间执行任务相比较 ScheduledExecutorService 而言优雅一点
  • ScheduledExecutorService 则胜在使用起来简洁,而且schedule方法可以提交Callable任务,并获取返回值
  • Thread#sleep方法,则尽量不要这么玩,有点违和

4. 高逼格的Spring定时器

Spring 相比较jdk自带的几种方式而言,我认为,最完美的有两点

  • 支持cron表达式
  • 注解方式,无侵入

配置xml文件

<task:executor id="executor" pool-size="5" />
<task:scheduler id="scheduler" pool-size="10" />
<task:annotation-driven executor="executor" scheduler="scheduler" />

实现业务逻辑

@Component
public class ScheduleDemo {
    @Scheduled(cron = "0 0 5 * * ?")
    public void doSome() {
        System.out.println(" 校验: " + System.currentTimeMillis());
    }

    
    @Scheduled(cron = "0 0 6 * * ?")
    public void alarm(){
        System.out.println(" 报警: " + System.currentTimeMillis());
    }
}

这个实现就简单了,相比较上面而言,添加一个注解,里面配置cron表达式,xml配置下,就可以实现定时任务

III. 小结

1. 本片博文主要介绍了实现定时任务的方式有几种,下面简单小结下四种方式的特点

方式

说明

特点

Thread#sleep

线程挂起一段时间

通过定时目标与当前时间计算sleepTime,来强制实现定时任务

Timer#TimerTask

异步定时任务

TimerTask内部实现定时任务逻辑 <br/>1. Timer可按频率调度任务 <br/> 2. Timer也支持指定时间调度任务

ScheduledExecutorService

计划任务线程池

1. 利用Executors#newScheduledThreadPool;创建线程池 <br/> 2. 创建线程任务实现定时任务逻辑 <br/> 3. 提交线程池执行,支持按频率调度,支持延迟多长时间调度 <br/> 4. 支持获取返回值

Spring Schedule

spring提供的定时任务

支持cron表达式,使用简单,非常简单,超级简单

2. 使用Timer方式,也可以算一种新的创建线程方式,

3. 使用小建议

不推荐使用 Thread#sleep的方式做定时任务

如指向利用jdk实现定时任务,可以考虑 TimerScheduledExecutorService

如项目本身就利用到了Spring,可以优先考虑这些优秀的框架提供的服务,用起来特别爽,谁用谁知道

IV. 其他

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如有问题,请不吝指正,感激

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏圣杰的专栏

ASP.NET Core 中断请求了解一下(翻译)

假设有一个耗时的Action,在浏览器发出请求返回响应之前,如果刷新了页面,对于浏览器(客户端)来说前一个请求就会被终止。而对于服务端来说,又是怎样呢?前一个请...

15330
来自专栏熊二哥

快速入门系列--WebAPI--04在老版本MVC4下的调整

WebAPI是建立在MVC和WCF的基础上的,原来微软老是喜欢封装的很多,这次终于愿意将http编程模型的相关细节暴露给我们了。在之前的介绍中,基本上都基于.N...

24860
来自专栏流柯技术学院

JMeter专题系列(三)元件的作用域与执行顺序

JMeter中共有8类可被执行的元件(测试计划与线程组不属于元件),这些元件中,取样器是典型的不与其它元件发生交互作用的元件,逻辑控制器只对其子节点的取样器有效...

12240
来自专栏Java编程技术

线程池使用FutureTask时候需要注意的一点事

线程池使用FutureTask的时候如果拒绝策略设置为了 DiscardPolicy和 DiscardOldestPolicy并且在被拒绝的任务的F...

13910
来自专栏分布式系统进阶

Influxdb 数据写入流程

因此对写入请求的处理就在函数 func (h *Handler) serveWrite(w http.ResponseWriter, r *http.Reque...

21330
来自专栏Linux驱动

第1阶段——uboot分析之启动函数bootm命令 (9)

本节主要学习: 详细分析UBOOT中"bootcmd=nand read.jffs2 0x30007FC0 kernel;bootm 0x30007FC0" 中...

29490
来自专栏MasiMaro 的技术博文

派遣函数

驱动程序的主要功能是用来处理IO请求,而大部分的IO请求是在派遣函数中完成的,用户模式下所有的IO请求都会被IO管理器封装为一个IRP结构,类似于Windows...

15410
来自专栏有趣的django

37.Django1.11.6文档

第一步 入门 检查版本 python -m django --version 创建第一个项目 django-admin startproject mysite ...

51980
来自专栏分布式系统进阶

ReplicaManager源码解析1-消息同步线程管理

基本上就是作三件事: 构造FetchRequest, 同步发送FetchRequest并接收FetchResponse, 处理FetchResponse, 这三...

17920
来自专栏Linux驱动

第1阶段——uboot分析之启动函数bootm命令 (9)

本节主要学习: 详细分析UBOOT中"bootcmd=nand read.jffs2 0x30007FC0 kernel;bootm 0x30007FC0...

20050

扫码关注云+社区

领取腾讯云代金券