java定时任务

java定时任务

核心算法

小顶堆

  • 定义

    • 完全二叉树
    • 父节点值小于等于子节点值为小顶堆
    • 父节点值大于等于子节点值为大顶堆
  • 存储

    • 链表

    • 数组

      数组可以很方便的获取下标,由于完全二叉树的特性,将根节点放在下标一位置,其子节点是2n和2n+1;某节点的父节点n/2

  • 堆元素上浮

    • 使用数组方式,每当有新元素加入时,放在队尾,然后和父节点比较,交换值
  • 堆元素下沉

    • 数组方式,每当删除一个堆顶元素时,将队尾元素放到第一个,然后和子节点比较,交换值
小顶堆算法劣势

小顶堆算法虽然能保证每次都能取到最近的时间,来执行定时任务,但是定时任务一旦多起来,就必须遍历所有的任务。像固定某些天执行的任务,比如每个月的1号执行的任务,其余日期就没必要去和它比较,每周一执行这样的任务,在周二到周日也没必要去参与下沉操作

时间轮算法

普通时间轮
  • 描述
    每个时间点存一个定时任务列表或数组,比如一天的12小时可以存12个队列,当时间到某一点时,取出对应的队列的任务来执行

  • 劣势

    假如任务间时间跨度太大,比如有些任务是每天0点0分0秒执行,有些是每年1月1号1点执行,中间还有无数其他任务,那么需要构建的队列的最小粒度是秒,最大粒度是年,一年有31536000秒;如果我希望任务每年1月1号执行一次,最大粒度就不只是年了,时间轮更难实现。

round时间轮
  • 描述

    每个任务加入一个round值,随后每次到了对应的点便减一,直到为0

  • 优势

    比较灵活,可以灵活配置各种时间粒度的任务

  • 劣势

    需要遍历所有任务,尽管可能无需执行

分层时间轮
  • 描述

    将时间轮分为日轮、月轮、周轮、年轮等,年轮记录哪个月执行,时间到了就丢给月轮,月轮记录几号执行,月轮到了就丢给日轮,日轮记录几点执行,可以丢给时轮…随后再执行,每个时间轮使用小顶堆来存储。cron表达式就是用的这种逻辑。

  • 优势

    比较灵活,可以灵活配置各种时间粒度的任务,并且没必要遍历一些无需遍历的任务。

java.util.timer

核心类

Timer
  • 实现定时任务调度,包含两个类(TaskQueue和TimerThread),包含固定频率调度和按实际成功时间调度

  • 核心函数sched(将任务添加到TaskQueue队列)

private void sched(TimerTask task, long time, long period)
  • 按实际执行成功时间调度
public void schedule(TimerTask task, Date firstTime, long period)
public void schedule(TimerTask task, long delay, long period)
  • 按预设时间调度
public void scheduleAtFixedRate(TimerTask task, long delay, long period) 
public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period) 
  • 如果是单线程实现定时任务,多个定时任务之间存在阻塞关系,无论采用哪种调度方式,线程都会阻塞,实际调度时间都在上一个任务结束后

    class TimeTask extends TimerTask {
        String name;
    
        public TimeTask(String name) {
            this.name = name;
        }
    
        @Override
        public void run() {
            try {
                System.out.println("name: "+name+";start time"+new Date());
                sleep(2000);
                System.out.println("name: "+name+";end time"+new Date());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    class SingleSchedule {
        public static void main(String[] args) {
            Timer timer = new Timer();
            for (int i = 0; i < 3; i++) {
                TimeTask timeTask = new TimeTask("testTask--" + i);
                // timer.schedule(timeTask, new Date(), 1000);
                timer.scheduleAtFixedRate(timeTask, new Date(), 1000);
            }
        }
    }
    name: testTask--0;start timeTue Apr 25 20:11:52 CST 2023
    name: testTask--0;end timeTue Apr 25 20:11:54 CST 2023
    name: testTask--1;start timeTue Apr 25 20:11:54 CST 2023
    name: testTask--1;end timeTue Apr 25 20:11:56 CST 2023
    name: testTask--2;start timeTue Apr 25 20:11:56 CST 2023
    name: testTask--2;end timeTue Apr 25 20:11:58 CST 2023
    name: testTask--0;start timeTue Apr 25 20:11:58 CST 2023
    name: testTask--0;end timeTue Apr 25 20:12:00 CST 2023
    name: testTask--1;start timeTue Apr 25 20:12:00 CST 2023
    name: testTask--1;end timeTue Apr 25 20:12:02 CST 2023
    name: testTask--2;start timeTue Apr 25 20:12:02 CST 2023
    name: testTask--2;end timeTue Apr 25 20:12:04 CST 2023
    name: testTask--1;start timeTue Apr 25 20:12:04 CST 2023
    
TaskQueue
  • Timer文件中的类,存储TimerTask任务队列,小顶堆数组
  • 队列初始大小128,自动扩容大小是原来的双倍
TimerThread
  • Timer文件中的类,继承了Thread类,用于多线程执行定时任务并设置下次执行时间

  • 核心方法mainLoop核心逻辑:

    • 任务的删除、下沉

    • 设置下次执行时间

      queue.rescheduleMin(
        task.period<0 ? currentTime   - task.period //实际执行成功时间调度
                      : executionTime + task.period);//按预设时间调度
      
TimerTask

抽象类实现Runnable接口,使用定时任务时需要新建一个类继承此类并实现run方法

主要算法

小顶堆(数组存储)
  • 初始化:大小128的定时任务数组

  • 扩容:每次扩容大小为之前的两倍

  • 上浮:新加入任务时将其加入到队尾,并不停与父节点比较,交换位置,直到符合小顶堆规则

  • 下沉:删除一个任务时,将其队尾任务替换,并循环与其子节点比较,交换位置,直到符合小顶堆规则

  • 更新:更新任务时,设置好其下次执行时间,然后下沉

使用线程池ScheduledThreadPoolExecutor

Leader-Follow模式

只有leader线程执行任务,其他为follower线程,leader线程执行完后,变为follower线程

避免没必要的唤醒和阻塞操作,高效且节约资源

ScheduledFutureTask

继承于FutureTask

DelayedWorkQueue

小顶堆,无界队列

改进

不会被别的任务阻塞,阻塞只会发生在自身任务

class TimeTask extends TimerTask {
    String name;

    public TimeTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        try {
            System.out.println("name: "+name+";start time"+new Date());
            sleep(2000);
            System.out.println("name: "+name+";end time"+new Date());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
class SingleSchedule {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 3; i++) {
            TimeTask timeTask = new TimeTask("testTask--" + i);
            // scheduledThreadPool.schedule(timeTask,1, TimeUnit.SECONDS);
            scheduledThreadPool.scheduleAtFixedRate(timeTask,0,1,TimeUnit.SECONDS);
        }
    }
}

name: testTask--2;start timeTue Apr 25 20:06:21 CST 2023
name: testTask--0;start timeTue Apr 25 20:06:21 CST 2023
name: testTask--1;start timeTue Apr 25 20:06:21 CST 2023
name: testTask--0;end timeTue Apr 25 20:06:23 CST 2023
name: testTask--2;end timeTue Apr 25 20:06:23 CST 2023
name: testTask--1;end timeTue Apr 25 20:06:23 CST 2023
name: testTask--2;start timeTue Apr 25 20:06:23 CST 2023
name: testTask--0;start timeTue Apr 25 20:06:23 CST 2023
name: testTask--1;start timeTue Apr 25 20:06:23 CST 2023
name: testTask--1;end timeTue Apr 25 20:06:25 CST 2023
name: testTask--2;end timeTue Apr 25 20:06:25 CST 2023
name: testTask--0;end timeTue Apr 25 20:06:25 CST 2023
name: testTask--2;start timeTue Apr 25 20:06:25 CST 2023
name: testTask--1;start timeTue Apr 25 20:06:25 CST 2023
name: testTask--0;start timeTue Apr 25 20:06:25 CST 2023
name: testTask--1;end timeTue Apr 25 20:06:27 CST 2023
name: testTask--0;end timeTue Apr 25 20:06:27 CST 2023

Quartz框架

简介

成熟的java定时任务框架

TriggerBuilder
JobDataMap
ScheduleBuilder
JobBuilder
JobDataMap
Job
JobDetail
Trigger
Listener
Scheduler
properties
ScheduleFatory

核心元素

JobDetail
Trigger
Scheduler
Listener
Cron表达式

quartz使用了时间轮和小顶堆算法,在CronExpression类中年月日时分秒各有自己的TreeSet

    private final String cronExpression;
    private TimeZone timeZone = null;
    protected transient TreeSet<Integer> seconds;
    protected transient TreeSet<Integer> minutes;
    protected transient TreeSet<Integer> hours;
    protected transient TreeSet<Integer> daysOfMonth;
    protected transient TreeSet<Integer> months;
    protected transient TreeSet<Integer> daysOfWeek;
    protected transient TreeSet<Integer> years;
字段   允许值   允许的特殊字符   
秒    0-59    , - * /   
分    0-59    , - * /   
小时    0-23    , - * /   
日期    1-31    , - * ? / L W C   
月份    1-12 或者 JAN-DEC    , - * /   
星期    1-7 或者 SUN-SAT    , - * ? / L C #   
年(可选)    留空, 1970-2099    , - * /   
  
  
表达式   意义   
"0 0 12 * * ?"    每天中午12点触发   
"0 15 10 ? * *"    每天上午10:15触发   
"0 15 10 * * ?"    每天上午10:15触发   
"0 15 10 * * ? *"    每天上午10:15触发   
"0 15 10 * * ? 2005"    2005年的每天上午10:15触发   
"0 * 14 * * ?"    在每天下午2点到下午2:59期间的每1分钟触发   
"0 0/5 14 * * ?"    在每天下午2点到下午2:55期间的每5分钟触发    
"0 0/5 14,18 * * ?"    在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发    
"0 0-5 14 * * ?"    在每天下午2点到下午2:05期间的每1分钟触发   
"0 10,44 14 ? 3 WED"    每年三月的星期三的下午2:10和2:44触发   
"0 15 10 ? * MON-FRI"    周一至周五的上午10:15触发   
"0 15 10 15 * ?"    每月15日上午10:15触发   
"0 15 10 L * ?"    每月最后一日的上午10:15触发   
"0 15 10 ? * 6L"    每月的最后一个星期五上午10:15触发     
"0 15 10 ? * 6L 2002-2005"    2002年至2005年的每月的最后一个星期五上午10:15触发   
"0 15 10 ? * 6#3"    每月的第三个星期五上午10:15触发    

特殊字符   意义   
*    表示所有值;   
?    表示未说明的值,即不关心它为何值;   
-    表示一个指定的范围;   
,    表示附加一个可能值;   
/    符号前表示开始时间,符号后表示每次递增的值;   
L("last")    ("last") "L" 用在day-of-month字段意思是 "这个月最后一天";用在 day-of-week字段, 它简单意思是 "7" or "SAT"。 如果在day-of-week字段里和数字联合使用,它的意思就是 "这个月的最后一个星期几" – 例如: "6L" means "这个月的最后一个星期五". 当我们用“L”时,不指明一个列表值或者范围是很重要的,不然的话,我们会得到一些意想不到的结果。   
W("weekday")    只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发。注意一点:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日。    
#    只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。    
C    指和calendar联系后计算过的值。例:在day-of-month 字段用“5C”指在这个月第5天或之后包括calendar的第一天;在day-of-week字段用“1C”指在这周日或之后包括calendar的第一天  

spring整合quartz

在springboot中,ScheduleFactoryBean可以自动注入一个默认的,并且可以隐式的调用Scheduler,从而使得不用显式去结合Job和Trigger。参考链接:

https://www.jianshu.com/p/52bf3f3aab6c

https://medium.com/@manvendrapsingh/quartz-scheduling-in-springboot-7cea1b7b19e7

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/quartz/SchedulerFactoryBean.html#setDataSource(javax.sql.DataSource)

配置特性文件

  • properties

    server.port=8000
    spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    spring.datasource.username=root
    spring.datasource.password=cc1234
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.platform=mysql
    spring.datasource.schema=classpath:tables_mysql.sql
    
    #quartz
    spring.quartz.auto-startup=true
    spring.quartz.job-store-type=jdbc
    spring.quartz.jdbc.initialize-schema=always
    spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
    spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_
    spring.quartz.properties.org.quartz.jobStore.isClustered=true
    spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=10000
    spring.quartz.properties.org.quartz.jobStore.useProperties=false
    spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
    spring.quartz.properties.org.quartz.threadPool.threadCount=10
    spring.quartz.properties.org.quartz.threadPool.threadPriority=5
    spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
    
    cron.test=*/5 * * * * ?
    
  • yml

server:
  port: 8862
  servlet:
    context-path: /quartzService
spring:
  application:
    name: quartzService
  datasource:
    quartz:
      #如果需要quartz 第一次运行时自动生成 quartz 所需的表那么后面的配置必须有:allowMultiQueries=true
      #待第一次运行后可以再根据自己的需要修改
      url: jdbc:mysql://localhost:3306/quartzJob?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: 123456

  quartz:
    #相关属性配置
    properties:
      org:
        quartz:
          scheduler:
            #调度器实例名称
            instanceName: clusteredScheduler
            #调度器实例编号自动生成
            instanceId: AUTO
          jobStore:
            #持久化方式配置
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            #持久化方式配置数据驱动,MySQL数据库
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            #quartz相关数据表前缀名
            tablePrefix: QRTZ_
            #开启分布式部署
            isClustered: true
            #分布式节点有效性检查时间间隔,单位:毫秒
            clusterCheckinInterval: 10000
            #配置是否使用
            useProperties: false
          threadPool:
            #线程池实现类
            class: org.quartz.simpl.SimpleThreadPool
            #执行最大并发线程数量
            threadCount: 10
            #线程优先级
            threadPriority: 5
            #配置是否启动自动加载数据库内的定时任务,默认true
            threadsInheritContextClassLoaderOfInitializingThread: true
    #数据库方式
    job-store-type: jdbc
    #初始化表结构
    jdbc:
      initialize-schema: always #always 属性意思是,每次初始化都会重新生成表(执行一次删除,执行一次创建),生成后,可以修改为 never

任务配置文件

@Configuration
public class QuartzConfig {
    @Value("${cron.test}")
    private String testCron;

    @Bean
    public JobDetail testJobDetail() {
        return JobBuilder.newJob()
            .ofType(TaskTestJob.class)
            .withIdentity("test1")
            .storeDurably()
            .build();
    }

    @Bean
    public Trigger testTrigger() {
        return TriggerBuilder.newTrigger()
            .withIdentity("test1")
            .forJob(testJobDetail())
            .withSchedule(CronScheduleBuilder.cronSchedule(testCron))
            .build();
    }
}

任务类

public class TaskTestJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        System.out.println("now time:" + new Date());
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }
}

执行结果分析

任务是多线程、并且是固定时间触发的

now time:Thu Apr 27 19:35:50 CST 2023
now time:Thu Apr 27 19:35:55 CST 2023
now time:Thu Apr 27 19:36:00 CST 2023
now time:Thu Apr 27 19:36:05 CST 2023
now time:Thu Apr 27 19:36:10 CST 2023
now time:Thu Apr 27 19:36:15 CST 2023
now time:Thu Apr 27 19:36:20 CST 2023
now time:Thu Apr 27 19:36:25 CST 2023
now time:Thu Apr 27 19:36:30 CST 2023
now time:Thu Apr 27 19:36:35 CST 2023
now time:Thu Apr 27 19:36:40 CST 2023
now time:Thu Apr 27 19:36:45 CST 2023
now time:Thu Apr 27 19:36:50 CST 2023
now time:Thu Apr 27 19:36:55 CST 2023
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Java中有多种方式可以实现定时任务。其中比较常用的两种方式是使用Timer和TimerTask类,以及使用ScheduledThreadPoolExecutor类。 1. 使用Timer和TimerTask类: Timer和TimerTask是Java中用于定时任务的类。Timer负责设定TimerTask的起始与间隔执行时间,而TimerTask是一个抽象类,需要实现自己的run方法,并通过Timer进行执行。下面是一个示例代码: ```java import java.time.LocalDateTime; import java.util.Timer; import java.util.TimerTask; public class Schedule { public static void main(String[] args) { TimerTask timerTask = new TimerTask() { @Override public void run() { System.out.println("当前线程:" + Thread.currentThread().getName() + " 当前时间:" + LocalDateTime.now()); } }; // 在指定延迟0毫秒后开始,随后每2000毫秒间隔执行timerTask new Timer().schedule(timerTask, 0L, 2000L); System.out.println("当前线程:" + Thread.currentThread().getName() + " 当前时间:" + LocalDateTime.now()); } } ``` 在上面的示例中,创建了一个TimerTask对象,并实现了run方法来定义定时任务的逻辑。然后通过Timer的schedule方法来指定任务的延迟执行时间和间隔执行时间。 2. 使用ScheduledThreadPoolExecutor类: Java 5.0引入的java.util.concurrent包中的ScheduledThreadPoolExecutor类可以实现更灵活的定时任务。它是一个线程池,用于以给定的速率或延迟重复执行任务。相比于Timer和TimerTask的组合,ScheduledThreadPoolExecutor允许多个服务线程,并且不需要子类TimerTask(只需实现Runnable接口)。下面是一个示例代码: ```java import java.time.LocalDateTime; import java.util.concurrent.*; public class Schedule { public static void main(String[] args) { // 创建一个ScheduledThreadPoolExecutor线程池,心线程数为5 ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(5); // 创建Runnable打印当前线程和当前时间 Runnable r = () -> System.out.println("当前线程:" + Thread.currentThread().getName() + " 当前时间:" + LocalDateTime.now()); /** * schedule:只执行一次调度 * scheduleAtFixedRate:一开始就计算间隔时间,如果任务超过间隔时间,那么就直接开始下一个任务 * scheduleWithFixedDelay:任务无论执行多久,都要等待上一轮任务完成之后再间隔指定时间,然后才开始下一个任务 */ // 在指定1秒延迟后执行r,之后每两秒执行一次 scheduledExecutorService.scheduleAtFixedRate(r, 1, 2, TimeUnit.SECONDS); } } ``` 在上面的示例中,首先创建了一个ScheduledThreadPoolExecutor线程池,核心线程数为5。然后创建一个Runnable对象,用于定义定时任务的逻辑。最后通过scheduleAtFixedRate方法来指定任务的延迟执行时间和间隔执行时间。 综上所述,Java中可以使用Timer和TimerTask类,以及ScheduledThreadPoolExecutor类来实现定时任务。选择哪种方式取决于具体的需求和场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值