zoukankan      html  css  js  c++  java
  • java定时任务调度框架

    java定时任务目前主要有三种:

    1. Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行;而且作业类需要集成java.util.TimerTask,一般用的较少。
    2. Spring3.0以后自带的task,即:spring schedule,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。
    3. Quartz,这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行;代码稍显复杂。

    定时器算法

    1.小顶堆

    堆,实际上是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左子节点和右子节点的值。

    堆又分为两种,最大堆、最小堆。
    - 最大堆: 任一非叶子节点的值均大于其左子节点和右子节点的值。根节点的值是最大的。
    - 最小堆: 任一非叶子节点的值均小于其左子节点和右子节点的值。根节点的值是最小的。

    小顶堆的实现方式

    由于堆是一种经过排序的完全二叉树,因此在构建的时候需要对其新插入的节点进行一些操作以使其符合堆的性质。这种操作就叫上浮与下沉。

    • 上浮:将当前节点与其父节点相比,如果当前节点的值比父节点小,就把当前节点与父节点交换,然后继续前面的交换,直到当前节点比父节点的值大为止。上浮就是将符合条件的节点往上移的过程。
    • 下沉:将当前节点与其左、右子节点相比,如果当前节点的值比其中一个或两个子节点的值大,就把当前节点与两个子节点种比较小的那个交换,,然后继续前面的比较,直到当前节点的值比两个子节点的值都小为止。下沉就是将符合条件的节点往下移的过程。

    2.时间轮算法

    见名知意,时间轮算法的数据结构类似于钟表上的数据指针,时间轮用环形数组的方式实现,数组中的每个元素都可以称之为槽(和redis集群的槽一样称呼)。槽的内部用双向链表存储着待执行的任务,添加和删除链表的操作时间复杂度为O(1),槽位本身也指代时间精度,比如一秒扫一个槽,那么这个时间轮的最高精度就是1秒。

    当有一个延迟任务要插入时间轮时,首先计算其延迟时间与单位时间的余值,从指针指向的当前槽位移动余值的个数槽位,就是该延迟任务需要被放入的槽位。

    举个例子,时间轮有8个槽位,编号为 0 ~ 7 。指针当前指向槽位 2 。新增一个延迟时间为 4 秒的延迟任务,4 % 8 = 4,因此该任务会被插入 4 + 2 = 6,也就是槽位6的延迟任务队列。

    时间槽位的实现方式

    时间轮的槽位实现可以采用循环数组的方式达成,也就是让指针在越过数组的边界后重新回到起始下标。概括来说,可以将时间轮的算法描述为:

    用队列来存储延迟任务,同一个队列中的任务,其延迟时间相同。用循环数组的方式来存储元素,数组中的每一个元素都指向一个延迟任务队列。有一个当前指针指向数组中的某一个槽位,每间隔一个单位时间,指针就移动到下一个槽位。被指针指向的槽位的延迟队列,其中的延迟任务全部被触发。在时间轮中新增一个延迟任务,将其延迟时间除以单位时间得到的余值,从当前指针开始,移动余值对应个数的槽位,就是延迟任务被放入的槽位。

    基于这样的数据结构,插入一个延迟任务的时间复杂度就下降到 O(1) 。而当指针指向到一个槽位时,该槽位连接的延迟任务队列中的延迟任务全部被触发。

    延迟任务的触发和执行不应该影响指针向后移动的时间精确性。因此一般情况下,用于移动指针的线程只负责任务的触发,任务的执行交由其他的线程来完成。比如,可以将槽位上的延迟任务队列放入到额外的线程池中执行,然后在槽位上新建一个空白的新的延迟任务队列用于后续任务的添加。

    代码实现

    Timer

    /**
     * @className: TimerTest
     * @description: 测试java.util.Timer的定时器实现
     * @author: charon
     * @create: 2021-10-10 10:35
     */
    public class TimerTest {
        public static void main(String[] args) {
            Timer timer = new Timer();
            // 延迟1s执行任务
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("延迟1s执行的任务"+new Date());
                }
            },1000);
            // 延迟3s执行任务,每隔5s执行一次
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("延迟3s每隔5s执行一次的任务"+new Date());
                }
            },3000,5000);
            // try {
            //     Thread.sleep(5000);
            // } catch (InterruptedException e) {
            //     e.printStackTrace();
            // }
            // timer.cancel();
            // System.out.println("任务执行完毕"+new Date());
        }
    }
    
    延迟1s执行的任务Sun Oct 10 14:34:13 CST 2021
    延迟3s执行的任务Sun Oct 10 14:34:15 CST 2021
    延迟3s执行的任务Sun Oct 10 14:34:20 CST 2021
    延迟3s执行的任务Sun Oct 10 14:34:25 CST 2021
    延迟3s执行的任务Sun Oct 10 14:34:30 CST 2021
    

    Timer的实现方式比较简单,其内部有两个主要的属性:

    /**
     * 用于存放定时任务TimeTask的列表
     */
    private final TaskQueue queue = new TaskQueue();
    /**
     * 用于执行定时任务的线程
     */
    private final TimerThread thread = new TimerThread(queue);
    

    TimerTask是一个实现了Runnable接口的抽象类。其run()方法用于提供具体的延时任务逻辑。

    TaskQueue内部采用的是小顶堆的算法实现。根据任务的触发时间采用死循环的方式进行排序,将执行时间最小的任务放在前面。

    void add(TimerTask task) {
        // Grow backing store if necessary
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);
    
        queue[++size] = task;
        fixUp(size);
    }
    
    private void fixUp(int k) {
        while (k > 1) {
            int j = k >> 1;
            if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }
    

    这样的方式就会有三个问题:

    1. 由于执行任务的线程只有一个,所以如果某个任务的执行时间过长,那么将破坏其他任务的定时精确性。如一个任务每1秒执行一次,而另一个任务执行一次需要5秒,那么如果是固定速率的任务,那么会在5秒这个任务执行完成后连续执行5次,而固定延迟的任务将丢失4次执行。
    2. 如果执行某个任务过程中抛出了异常,那么执行线程将会终止,导致Timer中的其他任务也不能再执行。
    3. Timer使用的是绝对时间,即是某个时间点,所以它执行依赖系统的时间,如果系统时间修改了的话,将导致任务可能不会被执行。

    由于Timer存在上面说的这些缺陷,在JDK1.5中,我们可以使用ScheduledThreadPoolExecutor来代替它,使用Executors.newScheduledThreadPool工厂方法或使用ScheduledThreadPoolExecutor的构造函数来创建定时任务,它是基于线程池的实现,不会存在Timer存在的上述问题,当线程数量为1时,它相当于Timer。

    schedule

    Spring Schedule在使用前都需要引入spring的包。

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    

    在这里我主要是使用spring boot注解的方式来实现:

    /**
     * 在spring boot的启动类上面添加 @EnableScheduling 注解
     */
    @SpringBootApplication
    @EnableScheduling
    public class ScheduleApplication {
        public static void main(String[] args) {
            SpringApplication.run(ScheduleApplication.class,args);
        }
    }
    

    新创建一个类,用来实现定时任务,这个类要注册成为Bean才行。

    /**
     * @className: ScheduleTest
     * @description: 测试schedule的执行器
     * @author: charon
     * @create: 2021-10-10 19:04
     */
    @Component
    public class ScheduleTest {
        /**
         * corn表达式:秒、分、时、日、月、星期
         *      值可以是数字,也可以是以下符号:
         *          *:所有值都匹配  示例: 0 0 * * * *:每小时(当秒和分都为0的时候)
         *          ?:只能用在日期和星期两个表达式种  示例: 0 0 12 * * ?   每天中午12点触发
         *          ,:或者  示例:0 0 9,13 * * *:每天的9点和13点
         *          /:增量值 示例: */10 * * * * *:每10秒
         *          -:区间  示例: 0 0/30 9-17 * * ? : 朝九晚五工作时间内每半小时
         */
        @Scheduled(cron="0 * * * * *")
        public void doSomething(){
            System.out.println("测试schedule的定时器,当秒为0的时候执行一次:"+new Date());
        }
    }
    
    测试schedule的定时器,当秒为0的时候执行一次:Sun Oct 10 19:18:00 CST 2021
    测试schedule的定时器,当秒为0的时候执行一次:Sun Oct 10 19:19:00 CST 2021
    测试schedule的定时器,当秒为0的时候执行一次:Sun Oct 10 19:20:00 CST 2021
    

    @Scheduled注解的另外两个重要属性:fixedRate和fixedDelay

    • fixedDelay:上一个任务结束后多久执行下一个任务
    • fixedRate:上一个任务的开始到下一个任务开始时间的间隔
    /**
     * 测试fixedRate,每2s执行一次
     * @throws Exception
     */
    @Scheduled(fixedRate = 2000)
    public void fixedRate() throws Exception {
        System.out.println("fixedRate开始执行时间:" + new Date(System.currentTimeMillis()));
        //休眠1秒
        Thread.sleep(1000);
        System.out.println("fixedRate执行结束时间:" + new Date(System.currentTimeMillis()));
    }
    
    fixedRate开始执行时间:Sun Oct 10 19:59:05 CST 2021
    fixedRate执行结束时间:Sun Oct 10 19:59:06 CST 2021
    fixedRate开始执行时间:Sun Oct 10 19:59:07 CST 2021
    fixedRate执行结束时间:Sun Oct 10 19:59:08 CST 2021
    fixedRate开始执行时间:Sun Oct 10 19:59:09 CST 2021
    fixedRate执行结束时间:Sun Oct 10 19:59:10 CST 2021
        
    /**
     * 等上一次执行完等待1s执行
     * @throws Exception
     */
    @Scheduled(fixedDelay = 1000)
    public void fixedDelay() throws Exception {
        System.out.println("fixedDelay开始执行时间:" + new Date(System.currentTimeMillis()));
        //休眠两秒
        Thread.sleep(1000 * 2);
        System.out.println("fixedDelay执行结束时间:" + new Date(System.currentTimeMillis()));
    }
    
    fixedDelay执行结束时间:Sun Oct 10 20:07:23 CST 2021
    fixedDelay开始执行时间:Sun Oct 10 20:07:24 CST 2021
    fixedDelay执行结束时间:Sun Oct 10 20:07:26 CST 2021
    fixedDelay开始执行时间:Sun Oct 10 20:07:27 CST 2021
    fixedDelay执行结束时间:Sun Oct 10 20:07:29 CST 2021
    

    如果是强调任务间隔的定时任务,建议使用fixedRate和fixedDelay,如果是强调任务在某时某分某刻执行的定时任务,建议使用cron表达式。

    Spring Schedule的Corn是使用的时间轮算法(分层时间轮,每个时间粒度对应一个时间轮,多个时间轮时间进行级联协调)。

    在CronSequenceGenerator.java这个类中,对每个CornTask都维护了一下7个Bitset(使用位数组而不用list,set之类的数据结构,一方面是因为空间效率,更重要的是接下来的操作主要是判断某个值是否匹配和从某个值开始找最近的下一个能够匹配的值)

    private final BitSet months = new BitSet(12);
    
    private final BitSet daysOfMonth = new BitSet(31);
    
    private final BitSet daysOfWeek = new BitSet(7);
    
    private final BitSet hours = new BitSet(24);
    
    private final BitSet minutes = new BitSet(60);
    
    private final BitSet seconds = new BitSet(60);
    

    然后根据配置的corn值计算这个任务对应的值计算每个bit的值。如我这里配置的每分钟执行一次的CornTask的结果如下:

    CronSequenceGenerator负责解析用户配置的Cron表达式,并提供next方法,根据给定的时间获取符合cron表达式规则的最近的下一个时间。CronTrigger实现Trigger的nextExecutionTime方法,根据定时任务执行的上下文环境(最近调度时间和最近完成时间)决定查找下一次执行时间的左边界,之后调用CronSequenceGenerator的next方法从左边界开始找下一次的执行时间。

    CronSequenceGenerator的doNext算法从指定时间开始(包括指定时间)查找符合cron表达式规则下一个匹配的时间。如图3-4所示,其整体思路是:沿着秒→分→时→日→月逐步检查指定时间的值。如果所有域上的值都已经符合规则那么指定时间符合cron表达式,算法结束。否则,必然有某个域的值不符合规则,调整该域到下一个符合规则的值(可能调整更高的域),并将较低域的值调整到最小值,然后从秒开始重新检查和调整。

    private void doNext(Calendar calendar, int dot) {
        List<Integer> resets = new ArrayList<>();
    
        int second = calendar.get(Calendar.SECOND);
        List<Integer> emptyList = Collections.emptyList();
        int updateSecond = findNext(this.seconds, second, calendar, Calendar.SECOND, Calendar.MINUTE, emptyList);
        if (second == updateSecond) {
            resets.add(Calendar.SECOND);
        }
    
        int minute = calendar.get(Calendar.MINUTE);
        int updateMinute = findNext(this.minutes, minute, calendar, Calendar.MINUTE, Calendar.HOUR_OF_DAY, resets);
        if (minute == updateMinute) {
            resets.add(Calendar.MINUTE);
        }
        else {
            doNext(calendar, dot);
        }
    
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        int updateHour = findNext(this.hours, hour, calendar, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_WEEK, resets);
        if (hour == updateHour) {
            resets.add(Calendar.HOUR_OF_DAY);
        }
        else {
            doNext(calendar, dot);
        }
    
        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
        int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
        int updateDayOfMonth = findNextDay(calendar, this.daysOfMonth, dayOfMonth, this.daysOfWeek, dayOfWeek, resets);
        if (dayOfMonth == updateDayOfMonth) {
            resets.add(Calendar.DAY_OF_MONTH);
        }
        else {
            doNext(calendar, dot);
        }
    
        int month = calendar.get(Calendar.MONTH);
        int updateMonth = findNext(this.months, month, calendar, Calendar.MONTH, Calendar.YEAR, resets);
        if (month != updateMonth) {
            if (calendar.get(Calendar.YEAR) - dot > 4) {
                throw new IllegalArgumentException("Invalid cron expression "" + this.expression +
                                                   "" led to runaway search for next trigger");
            }
            doNext(calendar, dot);
        }
    
    }
    
    private int findNext(BitSet bits, int value, Calendar calendar, int field, int nextField, List<Integer> lowerOrders) {
        int nextValue = bits.nextSetBit(value);
        // 下一个匹配值是-1,则将对更高的域做加1操作,从0开始查找下一个匹配值,将当前域设置为下一个匹配值,重置比当前域低的所有域设置为最小值,递归调度本算法。
        if (nextValue == -1) {
            calendar.add(nextField, 1);
            reset(calendar, Collections.singletonList(field));
            nextValue = bits.nextSetBit(0);
        }
        // 下一个匹配值不是当前值但也不是-1,则将当前域设置为下一个匹配值,将比当前域低的所有域设置为最小值,递归调度本算法
        if (nextValue != value) {
            calendar.set(field, nextValue);
            reset(calendar, lowerOrders);
        }
        // 下一个匹配值就是当前值,则匹配通过,如果当前域是月则算法结束,否则继续处理下一个更高的域。
        return nextValue;
    }
    

    Quartz

    在这里还是使用Spring Boot 集成Quartz;

    引入依赖:

     <dependency>
         <groupId>org.quartz-scheduler</groupId>
         <artifactId>quartz</artifactId>
         <version>2.3.0</version>
    </dependency>
    

    测试的job业务处理类:

    /**
     * @className: QuartzJob
     * @description: 业务逻辑处理类
     * @author: charon
     * @create: 2021-10-11 14:32
     */
    public class QuartzJob implements Job {
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            System.out.println("执行quartz定时器开始:" + new Date());
            // 模拟业务逻辑
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行quartz定时器结束:" + new Date());
        }
    }
    

    实例化Job,将任务触发器加入任务调度中:

    /**
     * @className: QuartzConfig
     * @description: scheduler的启动、结束等控制类
     * @author: charon
     * @create: 2021-10-11 14:38
     */
    @Configuration
    public class QuartzConfig {
    
        @Autowired
        private Scheduler scheduler;
    
        /**
         * 开始定时器
         */
        public void startJob() throws SchedulerException {
            // 通过JobBuilder构建JobDetail实例,JobDetail规定只能是实现Job接口的实例
            // JobDetail 是具体Job实例
            JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class).withIdentity("job", "group").build();
            // 基于表达式构建触发器   每5秒种执行一次
            CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
            // CronTrigger表达式触发器 继承于Trigger
            // TriggerBuilder 用于构建触发器实例
            CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("job1", "group1")
                    .withSchedule(cronScheduleBuilder).build();
            scheduler.scheduleJob(jobDetail, cronTrigger);
        }
    
        /**
         * 删除某个任务
         *
         * @param name job的名称
         * @param group job的分组
         * @throws SchedulerException
         */
        public void deleteJob(String name, String group) throws SchedulerException {
            JobKey jobKey = new JobKey(name, group);
            if (scheduler.checkExists(jobKey)){
                scheduler.deleteJob(jobKey);
            }
        }
    }
    

    测试类(spring容器初始化完成后执行):

    /**
     * @className: QuartzTest
     * @description: 测试quartz的定时器
     * @author: charon
     * @create: 2021-10-11 10:33
     */
    @Configuration
    public class QuartzTest implements ApplicationListener<ContextRefreshedEvent> {
    
        @Autowired
        private QuartzConfig quartzConfig;
    
        /**
         * 监听初始化quartz
         * @param event
         */
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
            System.out.println("容器初始化完成");
            try {
                quartzConfig.startJob();
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
        }
    }
    
    容器初始化完成
    2021-10-11 15:02:46.947  INFO 19628 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 7010 (http) with context path ''
    2021-10-11 15:02:49.558  INFO 19628 --- [           main] c.c.ScheduleApplication  : Started ScheduleApplication in 10.734 seconds (JVM running for 11.644)
    执行quartz定时器开始:Mon Oct 11 15:02:50 CST 2021
    执行quartz定时器结束:Mon Oct 11 15:02:52 CST 2021
    执行quartz定时器开始:Mon Oct 11 15:02:55 CST 2021
    执行quartz定时器结束:Mon Oct 11 15:02:57 CST 2021
    执行quartz定时器开始:Mon Oct 11 15:03:00 CST 2021
    执行quartz定时器结束:Mon Oct 11 15:03:02 CST 2021
    

    参考文章:

    https://blog.csdn.net/liuxiao723846/article/details/90546619

    https://www.cnblogs.com/WangJinYang/p/15160354.html

    https://blog.csdn.net/andy2019/article/details/79436869

    https://blog.csdn.net/weixin_42522400/article/details/112783846

    本文版权归Charon和博客园共有,原创文章,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    Java实现花朵数
    Java实现花朵数
    Java实现花朵数
    Java实现花朵数
    Java实现花朵数
    Java实现 黑洞数
    Java实现 黑洞数
    Java实现 黑洞数
    Could not create the view: An unexpected exception was thrown.问题解决
    让 SVN (TortoiseSVN)提交时忽略指定目录
  • 原文地址:https://www.cnblogs.com/pluto-charon/p/15451928.html
Copyright © 2011-2022 走看看