定时任务----时间轮算法
背景
在实际的业务场景中,我们常常需要周期性执行一些任务,比如巡查系统资源,处理过期数据等等。这些事情如果人工去执行的话,无疑是对人力资源的浪费。因此我们就开发出了定时任务。目前业界已有许多出色的定时任务框架,如quartz,elastic-job,包括SpringBoot也提供了定时任务,当然JDK本身也提供了定时任务功能。
那么我们在用这些框架的时候,有没有想过它们是怎么实现定时任务的呢?时间轮算法就是这样一种实现定时任务的方法。
概述
时间轮算法是通过一个时间轮去维护定时任务,按照一定的时间单位对时间轮进行划分刻度。然后根据任务的延时计算任务该落在时间轮的第几个刻度,如果任务时长超出了时间轮的刻度数量,则增加一个参数记录时间轮需要转动的圈数。
时间轮每转动一次就检查当前刻度下的任务圈数是否为0,如果为0说明时间到了就执行任务,否则就减少任务的圈数。这样看起来已经很好了,可以满足基本的定时任务需求了,但是我们还能不能继续优化一下呢?答案是可以的。想想我们家里的水表,它是不是有多个轮子在转动,时间轮是不是也可以改造成多级联动呢?建立3个时间轮,月轮、周轮、日轮,月轮存储每个月份需要执行定时任务,转动时将当月份的任务抛到周轮,周轮转动时将当天的任务抛到日轮中,日轮转动时直接执行当前刻度下的定时任务。
研究分析
笔者从github找了一个时间轮的项目,我们来研究一下时间轮的具体实现是怎样的。
https://github.com/ifesdjeen/hashed-wheel-timer.git
clone下来的项目目录是这样的,我们只需要查看core模块下的几个类就ok了。
HashedWheelTimer类就是实现时间轮的类,由于篇幅有限,下面只选取一些重要代码片段讲解
时间轮属性
public class HashedWheelTimer implements ScheduledExecutorService {
//时间轮默认转动一次的时间,10毫秒
public static final long DEFAULT_RESOLUTION = TimeUnit.NANOSECONDS.convert(10, TimeUnit.MILLISECONDS);
//时间轮默认尺寸
public static final int DEFAULT_WHEEL_SIZE = 512;
private static final String DEFAULT_TIMER_NAME = "hashed-wheel-timer";
//时间轮
private final Set<Registration<?>>[] wheel;
//时间轮尺寸
private final int wheelSize;
//转动一次花费的时间
private final long resolution;
//运转时间轮线程的执行器
private final ExecutorService loop;
//运转定时任务的执行器
private final ExecutorService executor;
//时间轮转动的等待策略
private final WaitStrategy waitStrategy;
//当前时间轮所在的刻度
private volatile int cursor = 0;
构造时间轮
public HashedWheelTimer(String name, long res, int wheelSize, WaitStrategy strategy, ExecutorService exec) {
this.waitStrategy = strategy;
this.wheel = new Set[wheelSize];
for (int i = 0; i < wheelSize; i++) {
wheel[i] = new ConcurrentSkipListSet<>();
}
this.wheelSize = wheelSize;
this.resolution = res;
final Runnable loopRunnable = new Runnable() {
@Override
public void run() {
long deadline = System.nanoTime();
while (true) {
// TODO: consider extracting processing until deadline for test purposes
Set<Registration<?>> registrations = wheel[cursor];
//遍历当前时间轮刻度中的所有任务
for (Registration r : registrations) {
if (r.isCancelled()) {
registrations.remove(r);
} else if (r.ready()) {
executor.execute(r);
registrations.remove(r);
if (!r.isCancelAfterUse()) {
reschedule(r);
}
} else {
r.decrement();
}
}
deadline += resolution;
try {
waitStrategy.waitUntil(deadline);
} catch (InterruptedException e) {
return;
}
cursor = (cursor + 1) % wheelSize;
}
}
};
//使用单线程池运行时间轮线程
this.loop = Executors.newSingleThreadExecutor(new ThreadFactory() {
AtomicInteger i = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, name + "-" + i.getAndIncrement());
thread.setDaemon(true);
return thread;
}
});
this.loop.submit(loopRunnable);
this.executor = exec;
}
加入定时任务
该方法用于加入初次延时与后续延时不一致的任务,另外两个一次性任务和固定频率的任务与该方法大同小异
private <V> Registration<V> scheduleFixedRate(long recurringTimeout,
long firstDelay,
Callable<V> callable) {
assertRunning();
//任务执行的时间间隔必须大于时间轮转动一次的时间
isTrue(recurringTimeout >= resolution,
"Cannot schedule tasks for amount of time less than timer precision.");
//根据任务延迟时间和时间轮转动一次的时间计算时间轮执行任务所需转动的刻度
int offset = (int) (recurringTimeout / resolution);
//根据时间轮执行任务所需转动的刻度和时间轮刻度数量计算所需执行任务所需转动的圈数
int rounds = offset / wheelSize;
//根据初始任务延迟时间和时间轮转动一次的时间计算时间轮初次执行任务所需转动的刻度
int firstFireOffset = (int) (firstDelay / resolution);
//根据时间轮初次执行任务所需转动的刻度和时间轮刻度数量计算所需初次执行任务所需转动的圈数
int firstFireRounds = firstFireOffset / wheelSize;
Registration<V> r = new FixedRateRegistration<>(firstFireRounds, callable, recurringTimeout, rounds, offset);
//刻度总是+1,因为firstFireOffset是两个整数相除的结果,会出现精度丢失的情况。因此对刻度直接+1进行补偿.
//并且+1也可以避免出现时间轮已遍历完当前刻度的所有任务,正处于等待状态时,外部在当前刻度加入新的任务,导致任务延迟一圈
wheel[idx(cursor + firstFireOffset + 1)].add(r);
return r;
}
Registration接口是基础定时任务,所有的定时任务类都需要实现这个接口
interface Registration<T> extends ScheduledFuture<T>, Runnable {
enum Status {
CANCELLED,
READY
// COMPLETED ??
}
//距离执行任务的时间轮圈数
int rounds();
/**
* Decrement an amount of runs Registration has to run until it's elapsed
* 减少剩余执行圈数
*/
void decrement();
/**
* Check whether the current Registration is ready for execution
*检查当前任务是否进入准备执行状态
* @return whether or not the current Registration is ready for execution
*/
boolean ready();
/**
* Reset the Registration
* 重置当前任务的状态
*/
void reset();
//取消定时任务
boolean cancel(boolean mayInterruptIfRunning);
/**
* Check whether the current Registration is cancelled
*检查任务是否取消
* @return whether or not the current Registration is cancelled
*/
boolean isCancelled();
//检查任务是否已完成
boolean isDone();
/**
* Get the offset of the Registration relative to the current cursor position
* to make it fire timely.
*获取任务所在的刻度
* @return the offset of current Registration
*/
int getOffset();
//任务执行后是否需要移除
boolean isCancelAfterUse();
//获取任务的执行间隔时间,以纳秒为单位
long getDelay(TimeUnit unit);
@Override
default int compareTo(Delayed o) {
Registration other = (Registration) o;
long r1 = rounds();
long r2 = other.rounds();
if (r1 == r2) {
return other == this ? 0 : -1;
} else {
return Long.compare(r1, r2);
}
}
@Override
T get() throws InterruptedException, ExecutionException;
@Override
T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
FixedDelayRegistration,FixedRateRegistration,OneShotRegistration三个类分别是初次延迟后续固定频率定时任务,固定频率定时任务,一次性定时任务。这些类没有什么好讲的都是实现Registration或继承OneShotRegistration类重写一些方法而已。
时间轮等待策略
public interface WaitStrategy {
/**
* Wait until the given deadline, deadlineNanoseconds
*
* @param deadlineNanoseconds deadline to wait for, in milliseconds
*/
public void waitUntil(long deadlineNanoseconds) throws InterruptedException;
/**
* Yielding wait strategy.
*
* Spins in the loop, until the deadline is reached. Releases the flow control
* by means of Thread.yield() call. This strategy is less precise than BusySpin
* one, but is more scheduler-friendly.
* 线程让度等待策略
*/
public static class YieldingWait implements WaitStrategy {
@Override
public void waitUntil(long deadline) throws InterruptedException {
while (deadline >= System.nanoTime()) {
Thread.yield();
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
}
}
}
/**
* BusySpin wait strategy.
*
* Spins in the loop until the deadline is reached. In a multi-core environment,
* will occupy an entire core. Is more precise than Sleep wait strategy, but
* consumes more resources.
* 通过空自旋策略阻塞线程,达到时间轮的轮转效果,消耗的资源比睡眠策略多,但是要更准确
*/
public static class BusySpinWait implements WaitStrategy {
@Override
public void waitUntil(long deadline) throws InterruptedException {
Long sd=System.nanoTime();
while (deadline >= System.nanoTime()) {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
}
Long ed=System.nanoTime();
// System.out.println("等待时间:"+(ed-sd));
}
}
/**
* Sleep wait strategy.
*
* Will release the flow control, giving other threads a possibility of execution
* on the same processor. Uses less resources than BusySpin wait, but is less
* precise.
* 通过线程休眠的方式实现时间轮的跳动,比BusySpin方式占用的资源少,但是没有它那么精准
*/
public static class SleepWait implements WaitStrategy {
@Override
public void waitUntil(long deadline) throws InterruptedException {
long sleepTimeNanos = deadline - System.nanoTime();
if (sleepTimeNanos > 0) {
long sleepTimeMillis = sleepTimeNanos / 1000000;
int sleepTimeNano = (int) (sleepTimeNanos - (sleepTimeMillis * 1000000));
Thread.sleep(sleepTimeMillis, sleepTimeNano);
}
}
}
}
到这里整个项目的讲解基本就已经结束了,在core模块的test目录下有测试方法,读者可以自行测试项目。