【小工具】定时任务执行器

背景

有时我们的项目内需要一个定时执行器来执行某些任务,就需要一个简单好用的定时任务机。
注意,这个定时任务机并不原生支持分布式,如果需要分布式的功能请自己实现。

版本

  • jdk21

代码

Job

用于统一封装需要执行的任务和开始时间、间隔时间

import lombok.Getter;
import java.util.Objects;

/**
 * 任务封装
 */
@Getter
public class Job implements Comparable<Job> {

    /**
     * 待执行任务
     */
    private Runnable task;
    /**
     * 下次开始时间
     */
    private long startTime;
    /**
     * 需要等待时间
     */
    private long delay;


    /**
     * 私有无参构造器,防止外部调用
     */
    private Job() {
    }

    /**
     * 从startTime开始,每隔delay毫秒执行一次task
     *
     * @param task      待执行任务
     * @param startTime 开始时间
     * @param delay     等待时间
     */
    public Job(Runnable task, long startTime, long delay) {
        if (Objects.isNull(task)) {
            throw new IllegalArgumentException("待执行任务不能为Null");
        }
        if (startTime <= 0) {
            throw new IllegalArgumentException("开始时间非法");
        }
        if (delay <= 0) {
            throw new IllegalArgumentException("等待时间非法");
        }
        this.task = task;
        this.startTime = startTime;
        this.delay = delay;
    }

    /**
     * 用于排序任务
     *
     * @param o the object to be compared.
     * @return 排序结果
     */
    @Override
    public int compareTo(Job o) {
        return Long.compare(this.startTime, o.startTime);
    }
}

Job执行机

import com.utils.ScheduleUtil.Job;
import org.slf4j.Logger;

import java.util.concurrent.*;
import java.util.concurrent.locks.LockSupport;

public class MineSchedule {

    // 注意,这里的线程池默认只给了6个空间,是为了方便学习。实际生产中应当做更精确的线程池,比如用google提供的线程池创建工具
    private final ExecutorService service = Executors.newFixedThreadPool(6);
    private final Trigger trigger = new Trigger();

    class Trigger {

        private static final Logger log = org.slf4j.LoggerFactory.getLogger(Trigger.class);

        /**
         * 优先级队列,会自动排序
         */
        PriorityBlockingQueue<Job> queue = new PriorityBlockingQueue<>();

        Thread machine = new Thread(() -> {
            while (true) {
                // 如果队列中没有任务,就park
                while (queue.isEmpty()) {
                    log.info("队列中没有任务,线程park");
                    LockSupport.park();
                }
                // 如果队列中有任务,就取出最早的任务,判断是否到时间了,如果到时间了,就执行任务,否则就park
                // peek和poll的区别是,peek不会删除元素,poll会删除元素
                // 所以用peek先把队列的头部取出来看一眼时间做if判断
                Job latelyJob = queue.peek();
                if (latelyJob.getStartTime() < System.currentTimeMillis()) {
                    // 需要执行时才poll出来执行
                    latelyJob = queue.poll();
                    if (latelyJob != null) {
                        service.execute(latelyJob.getTask());
                        queue.offer(rebuildJob(latelyJob));
                    }
                } else {
                    LockSupport.parkUntil(latelyJob.getStartTime());
                }
            }
        }, "scheduler-machine");

        {
            machine.start();
            log.info("触发器启动");
        }

        // 添加任务立即执行一次,所以需要一个强制唤醒
        void wakeUp() {
            LockSupport.unpark(machine);
        }

        // 任务重新放回队列,等候下一次执行
        private Job rebuildJob(Job old) {
            return new Job(old.getTask(), old.getStartTime() + old.getDelay(), old.getDelay());
        }

    }

    /**
     * 每隔delay毫秒数,自动执行一次task
     *
     * @param task  需要周期执行的任务
     * @param delay 延迟时间
     */
    public void schedule(Runnable task, long delay) {
        // 最开始的想法,搞一个线程池,每次有新任务的时候把任务丢进去,睡delay毫秒后执行
        // 但是这是有问题的,线程耗尽就完了,而且线程不可复用,创建线程消耗资源很大
        // 那我们就考虑这么一种设计:
        // 1. 有一个定时触发器,每隔delay时间被唤醒,然后去尝试执行任务
        // 2. 线程池只负责执行任务,不负责处理时间
        // 那么这个触发器需要什么信息呢?第一,所有需要执行的任务,第二,需要delay的时间
        // 那么我们封装一个Job类,专门用来记录任务和时间
        // 再写一个trigger,用于时间触发
        Job job = new Job(task, System.currentTimeMillis(), delay);
        trigger.queue.offer(job);
        trigger.wakeUp();
    }
}

引入虚拟线程及功能加强

Job增加次数限制和时间限制并记录已执行次数

@Getter
@Builder
public class Job implements Comparable<Job> {

    /**
     * 待执行任务
     */
    private Runnable task;
    /**
     * 下次开始时间
     */
    private long startTime;
    /**
     * 需要等待时间
     */
    private long delay;
    /**
     * 最多执行次数, -1为不生效
     */
    private int limitTimes;
    /**
     * 已经执行次数
     */
    private int doneTimes;
    /**
     * 最后执行到什么时候,-1为不生效
     */
    private long lastExecuteTime;


    /**
     * 私有无参构造器,防止外部调用
     */
    private Job() {
    }

    /**
     * 从startTime开始,每隔delay毫秒执行一次task
     *
     * @param task      待执行任务
     * @param startTime 开始时间
     * @param delay     等待时间
     */
    public Job(Runnable task, long startTime, long delay, int limitTimes, int doneTimes, long lastExecuteTime) {
        if (Objects.isNull(task)) {
            throw new IllegalArgumentException("待执行任务不能为Null");
        }
        if (startTime <= 0) {
            throw new IllegalArgumentException("开始时间非法");
        }
        if (delay <= 0) {
            throw new IllegalArgumentException("等待时间非法");
        }
        this.task = task;
        this.startTime = startTime;
        this.delay = delay;
        this.limitTimes = limitTimes;
        this.doneTimes = doneTimes;
        this.lastExecuteTime = lastExecuteTime;
    }

    /**
     * 用于排序任务
     *
     * @param o the object to be compared.
     * @return 排序结果
     */
    @Override
    public int compareTo(Job o) {
        return Long.compare(this.startTime, o.startTime);
    }
}

虚拟线程实现并增加手动关停

public class SchedulerUtil {

    private final ExecutorService service = Executors.newVirtualThreadPerTaskExecutor();
    private final Trigger trigger = new Trigger();

    class Trigger {

        private static final Logger log = org.slf4j.LoggerFactory.getLogger(Trigger.class);

        /**
         * 优先级队列,会自动排序
         */
        PriorityBlockingQueue<Job> queue = new PriorityBlockingQueue<>();
        AtomicBoolean running = new AtomicBoolean(true);

        Thread machine = Thread.ofVirtual().unstarted(() -> {
            while (running.get()) {
                // 如果队列中没有任务,就park
                while (queue.isEmpty()) {
                    log.info("队列中没有任务,线程park");
                    LockSupport.park();
                }
                // 如果队列中有任务,就取出最早的任务,判断是否到时间了,如果到时间了,就执行任务,否则就park
                // peek和poll的区别是,peek不会删除元素,poll会删除元素
                // 所以用peek先把队列的头部取出来看一眼时间做if判断
                Job latelyJob = queue.peek();
                if (latelyJob.getStartTime() < System.currentTimeMillis()) {
                    // 需要执行时才poll出来执行
                    latelyJob = queue.poll();
                    if (latelyJob != null) {
                        service.execute(latelyJob.getTask());
                        Optional<Job> job = rebuildJob(latelyJob);
                        job.ifPresent(value -> queue.offer(value));
                    }
                } else {
                    LockSupport.parkUntil(latelyJob.getStartTime());
                }
            }
        });

        {
            machine.start();
            log.info("触发器启动");
        }

        // 添加任务立即执行一次,所以需要一个强制唤醒
        void wakeUp() {
            LockSupport.unpark(machine);
        }

        // 任务重新放回队列,等候下一次执行
        private Optional<Job> rebuildJob(Job old) {
            if (old.getLimitTimes() > 0 && old.getDoneTimes() + 1 >= old.getLimitTimes()) {
                return Optional.empty();
            }
            if (old.getLastExecuteTime() > 0 && old.getStartTime() + old.getDelay() >= old.getLastExecuteTime()) {
                return Optional.empty();
            }
            return Optional.of(
                    new Job(
                            old.getTask(),
                            old.getStartTime() + old.getDelay(),
                            old.getDelay(),
                            old.getLimitTimes(),
                            old.getDoneTimes() + 1,
                            old.getLastExecuteTime()
                    )
            );
        }

    }

    /**
     * 每隔delay毫秒数,自动执行一次task
     *
     * @param task            需要周期执行的任务
     * @param delay           延迟时间
     * @param limitTimes      限制次数
     * @param doneTimes       已经执行次数
     * @param lastExecuteTime 最后执行到什么时候
     */
    public void schedule(Runnable task, long delay, int limitTimes, int doneTimes, long lastExecuteTime) {
        Job job = new Job(task, System.currentTimeMillis(), delay, limitTimes, doneTimes, lastExecuteTime);
        trigger.queue.offer(job);
        trigger.wakeUp();
    }

    // 新增资源关闭方法
    public void shutdown() {
        trigger.running.compareAndSet(true, false);
        service.close();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值