Java定时任务技术分析

Java
279
0
0
2023-07-15
标签   Java任务

常见的业务场景:

  • 某博客平台,支持定时发送文章。
  • 某学习平台,定时发送学习任务通知用户
  • 定时进行数据抓取等等

在项目中要求我们在某个时刻去做某件事情,下面我们就来看看有哪些方法可以实现定时任务。

JDK 内置类

Timer

java .util.Timer是 JDK 1.3 开始就已经支持的一种定时任务的实现方式。

Timer 内部使用一个叫做 TaskQueue 的类存放定时任务,它是一个基于最小堆实现的优先级队列。 TaskQueue 会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!由于某个任务的执行时间可能较长,则后面的任务运行的时间会被延迟,所以执行的时间和你预期的时间可能不一致。延迟的任务具体开始的时间,就是依据前面任务的结束时间。

核心方法:

 //启动任务之后,延迟多久时间执行
void schedule(TimerTask task, long delay);
//在指定的时间执行任务
void schedule(TimerTask task, Date time);
//启动任务后,延迟多久时间执行,执行之后指定间隔多久重复执行任务
void schedule(TimerTask task, long delay, long period);
//指定时间启动任务,执行后间隔指定时间重复执行任务
void schedule(TimerTask task, Date firstTime, long period); 

代码案例:

 public class TimerUse {

  public  static  void main(String[] args) {
    System.out.println("当前时间: " + new Date() + "n" +
        " 线程 名称: " + Thread.currentThread().getName());

    testTimer();
        //    testTimer();
        //    testTimer();
        //    testTimer();
  }

  // 方法一:设定指定任务task在指定时间time执行 schedule(TimerTask task, long delay)
  public static void testTimer() {
    Timer timer = new Timer("Timer");
    timer.schedule(new TimerTask() {
      public void run() {
        System.out.println("当前时间: " + new Date() + "n" +
            "线程名称: " + Thread.currentThread().getName());
      }
    },);
    // 设定指定的时间time为毫秒
  }

  /**
   * 方法二:设定指定任务task在指定延迟delay后间隔指定时间peroid执行 schedule(TimerTask task, long delay, long period)
   */
  public static void testTimer() {
    Timer timer = new Timer("Timer");
    timer.schedule(new TimerTask() {
      public void run() {
        System.out.println("当前时间: " + new Date() + "n" +
            "线程名称: " + Thread.currentThread().getName());
      }
    },, 3500);
  }


  /**
   * 方法三:在指定的时间执行任务 schedule(TimerTask task, Date time)
   */

  public static void testTimer() {
    Date date = new Date();
    Calendar calendar = Calendar. getInstance ();
    calendar.setTime(date);
    calendar.add(Calendar.MINUTE,); // 往后推一分钟

    Date time = calendar.getTime();    //获取当前系统时间

    Timer timer = new Timer("Timer");
    timer.schedule(new TimerTask() {
      public void run() {
        System.out.println("当前时间: " + new Date() + "n" +
            "线程名称: " + Thread.currentThread().getName());
      }
    }, time);
  }

  /**
   * 方法四:安排指定的任务task在指定的时间firstTime开始进行重复的固定速率period执行. schedule(TimerTask task, Date firstTime,
   * long period)
   */
  public static void testTimer() {
    Calendar calendar = Calendar.getInstance();
    calendar.set(Calendar.HOUR_OF_DAY,); // 控制小时
    calendar.set(Calendar.MINUTE,);    // 控制分钟
    calendar.set(Calendar.SECOND,);    // 控制秒

    Date time = calendar.getTime();    //获取当前系统时间

    Timer timer = new Timer("Timer");
    timer.schedule(new TimerTask() {
      public void run() {
        System.out.println("当前时间: " + new Date() + "n" +
            "线程名称: " + Thread.currentThread().getName());
      }
    }, time, * 60 * 60 * 24);// 这里设定将延时每天固定执行
  }
} 

注意事项

1、创建一个 Timer 对象相当于新启动了一个线程,但是这个新启动的线程,并不是守护线程。它一直在后台运行,通过如下代码将新启动的 Timer 线程设置为守护线程。

 Timer timer = new Timer(true); 

变为守护线程,则意味着主线程执行结束,则程序就结束了,定时任务也就不会执行。

2、当计划时间早于当前时间,则任务立即被运行。

ScheduledExecutorService

ScheduledExecutorService 是一个接口,有多个实现类,比较常用的是 ScheduledThreadPoolExecutor 。

 public class ScheduledThreadPoolExecutor 
   extends  ThreadPoolExecutor implements ScheduledExecutorService {} 

ScheduledThreadPoolExecutor 的状态管理、入队操作、拒绝操作等都是继承于 ThreadPoolExecutor ;ScheduledThreadPoolExecutor 主要是提供了周期任务和延迟任务相关的操作;

 schedule(Runnable command, long delay, TimeUnit unit) // 无返回值的延迟任务
schedule(Callable callable, long delay, TimeUnit unit) // 有返回值的延迟任务
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) // 固定频率周期任务
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) // 固定延迟周期任务 

代码示例:

 TimerTask repeatedTask = new TimerTask() {
  @SneakyThrows
  public void run() {
    System.out.println("当前时间: " + new Date() + "n" +
                       "线程名称: " + Thread.currentThread().getName());
  }
};
System.out.println("当前时间: " + new Date() + "n" +
                   "线程名称: " + Thread.currentThread().getName());
ScheduledExecutorService executor = Executors.newScheduledThreadPool();
long delay =L;
long period =L;
// 延迟s,周期2s
executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.SECONDS);
// 定时任务重复执行个周期
Thread.sleep((delay + period *) * 1000);
executor.shutdown(); 

执行结果为:

 当前时间: Wed Nov 09:52:35 CST 2022n线程名称: main
当前时间: Wed Nov 09:52:36 CST 2022n线程名称: pool-1-thread-1
当前时间: Wed Nov 09:52:38 CST 2022n线程名称: pool-1-thread-1
当前时间: Wed Nov 09:52:40 CST 2022n线程名称: pool-1-thread-1
当前时间: Wed Nov 09:52:42 CST 2022n线程名称: pool-1-thread-1 

注意事项

1、scheduleAtFixedRate 和 scheduleWithFixedDelay 是我们最常用的两个方法,两者略有区别,前者为固定频率周期任务,如果任务执行时间超出周期时,下一次任务会立刻运行;后者为固定延迟周期任务, 无论执行时间是多少,其结果都是在执行完毕后,停顿固定的时间,然后执行下一次任务

2、ScheduledThreadPoolExecutor 线程最多为核心线程,最大线程数不起作用,因为 DelayedWorkQueue 是无界队列。

更多内容推荐阅读:并发系列(7)之 ScheduledThreadPoolExecutor 详解

小结

在 JDK 中,内置了两个类,可以实现定时任务的功能:

  • java.util.Timer :可以通过创建 java.util.TimerTask 调度任务,在同一个线程中 串行 执行,相互影响。也就是说,对于同一个 Timer 里的多个 TimerTask 任务,如果一个 TimerTask 任务在执行中,其它 TimerTask 即使到达执行的时间,也只能排队等待。因为 Timer 是串行的,同时存在 坑坑 ,所以后来 JDK 又推出了 ScheduledExecutorService ,Timer 也基本不再使用。
  • java.util.concurrent.ScheduledExecutorService :在 JDK 1.5 新增,基于 线程池 设计的定时任务类,每个调度任务都会被分配到线程池中 并发 执行,互不影响。这样,ScheduledExecutorService 就解决了 Timer 串行的问题。

在日常开发中,我们很少直接使用 Timer 或 ScheduledExecutorService 来实现定时任务的需求。主要有几点原因:

  • 它们仅支持按照指定频率,不直接支持指定时间的定时调度,需要我们结合 Calendar 自行计算,才能实现复杂时间的调度。例如说,每天、每周五、2019-11-11 等等,不支持 Cron 表达式。
  • 它们是进程级别,而我们为了实现定时任务的高可用,需要部署多个进程。此时需要等多考虑,多个进程下,同一个任务在相同时刻,不能重复执行。
  • 项目可能存在定时任务较多,需要统一的管理,此时不得不进行二次封装。

所以,一般情况下,我们会选择专业的 调度任务中间件

中间件

Spring Task

由于 SpringTask 已经存在于 Spring 框架中,所以无需添加依赖。

下面我们弄个小 Demo 测试一下,新建一个项目,引入依赖。

 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>.6.3</version>
  <relativePath/>
</parent>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>.18.20</version>
  </dependency>
</dependencies> 

添加 SpringTask 的配置类

 @Configuration
@EnableScheduling
public class SpringTaskConfig {

} 

在 application.yml 添加关于 Spring Task 的配置,如下:

 spring:
  task:
    # Spring Task 调度任务的配置,对应 TaskSchedulingProperties 配置类
    scheduling:
      thread-name-prefix: job- # 线程池的线程名的前缀。默认为 scheduling- ,建议根据自己应用来设置
      pool:
        size: # 线程池大小。默认为 1 ,根据自己应用来设置
       shutdown :
        await-termination: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
        await-termination-period: # 等待任务完成的最大时长,单位为秒。默认为 0 ,根据自己应用来设置 

Spring .task.scheduling.shutdown 配置项,是为了实现 Spring Task 定时任务的优雅关闭。

定时任务测试类

 @Service
@Slfj
public class ScheduledTaskService {

  private final AtomicInteger counts = new AtomicInteger();

  //  @Scheduled(cron = " 0/10 * ? * ?")//每10分钟执行一次
  @Scheduled(fixedRate =) // 每 3秒执行一次
  public void pushMessage() {
    log.info("[execute]定时第({})给用户发送通知", counts.incrementAndGet());
  }

} 

最后创建一个启动类,启动项目,控制台输出如下:

Spring Task 支持 Cron 表达式 。Cron 表达式主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作。

Cron 格式中每个时间元素的说明

平时可以找一个 Cron 表达式生成器在线网站按需生成想要的表达式。

SpringTask 功能小结:

1、SpringTask 内置于 Spring 框架,相比于 Quartz 更加简单方便,不需要引入其他依赖。

2、Spring Task 底层是基于 JDK 的 ScheduledThreadPoolExecutor 线程池来实现的。

3、支持 Cron 表达式

4、只支持单机,功能单一

quartz

Github:github.com/quartz-sche…

Quartz 作为一个优秀的开源调度框架,Quartz 具有以下特点:

  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  3. 分布式和集群能力, Terracotta 收购后在原来功能基础上作了进一步提升。

另外,作为 Spring 默认的调度框架,Quartz 很容易与 Spring 集成实现灵活可配置的调度功能。

在 Quartz 体系结构中,有三个组件非常重要:

  • Scheduler :调度器。Scheduler启动Trigger去执行Job。
  • Trigger :触发器。用来定义 Job(任务)触发条件、触发时间,触发间隔,终止时间等。四大类型:SimpleTrigger(简单的触发器)、CornTrigger(Cron表达式触发器)、DateIntervalTrigger(日期触发器)、CalendarIntervalTrigger(日历触发器)。
  • Job :任务。具体要执行的业务逻辑,比如:发送短信、发送邮件、访问数据库、同步数据等。

Quartz 应用分为单机模式和集群模式。实际应用中,我们都会选择集群模式,关于 Quartz 的使用,后续会单独出一篇文章进行介绍。

Quartz 框架出现的比较早,后续不少定时框架,或多或少都基于 Quartz 研发的,比如 当当网 的elastic-job就是基于quartz二次开发之后的分布式调度解决方案。

并且,Quartz 并没有内置 UI 管理控制台,不过你可以使用 quartzui 这个开源项目来解决这个问题。

Quartz 虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。

Quartz 优缺点:

  • 可以与 Spring 集成,并且支持动态添加任务和集群。
  • 分布式支持不友好,没有内置 UI 管理控制台、使用麻烦(相较于其他框架)。

XXL-JOB

官方说明:XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

通俗来讲:XXL-JOB 是一个任务调度框架,通过引入 XXL-JOB 相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的 启动 执行 中止 以及包含了 日志记录与查询 任务状态监控

特性:

Xxl-job 解决了很多 Quartz 的不足。

XXL-JOB 的架构设计如下图所示:

从上图可以看出,XXL-JOB 由 调度中心 执行器 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。

关于 xxl-job 的使用,下篇文章会详细介绍的。

xxl-job 的优点相对于 Quartz 非常明显,使用更加简单,而且内置了 UI 管理控制台。

Elastic-Job…

ElasticJob 是面向互联网生态和海量任务的分布式调度解决方案,基于Quartz和 ZooKeeper 开发,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 它通过弹性调度、资源管控、以及作业治理的功能,打造一个适用于互联网场景的分布式调度解决方案,并通过开放的架构设计,提供多元化的作业生态。 它的各个产品使用统一的作业 API,开发者仅需一次开发,即可随意部署。

ElasticJob-Lite 的架构设计如下图所示:

从上图可以看出,Elastic-Job 没有调度中心这一概念,而是使用 ZooKeeper 作为注册中心,注册中心负责协调分配任务到不同的节点上。

Elastic-Job 中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。

 @Component
@ElasticJobConf(name = "dayJob", cron = "/10 * * * * ?",  Sharding TotalCount = 2,
        shardingItemParameters = "=AAAA,1=BBBB", description = "简单任务", failover = true)
public class TestJob implements SimpleJob {
    @Override
    public void execute(ShardingContext shardingContext) {
        log.info("TestJob任务名:【{}】, 片数:【{}】, param=【{}】", shardingContext.getJobName(), shardingContext.getShardingTotalCount(),
                shardingContext.getShardingParameter());
    }
} 

Elastic-Job 支持的功能:

关于 Elastic-Job 的使用,未来会抽时间出一篇文章的。

Elastic-Job 相较于 XXL-JOB 缺点也比较明显,就是需要引入额外的中间件,比如 Zookeeper,增加了操作难度。