背景
有时我们的项目内需要一个定时执行器来执行某些任务,就需要一个简单好用的定时任务机。
注意,这个定时任务机并不原生支持分布式,如果需要分布式的功能请自己实现。
版本
- 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();
}
}