时间轮算法 简单学习
时间轮
参考: https://github.com/wolaiye1010/zdc-java-script/
参考: https://www.cnblogs.com/zhongwencool/p/timing_wheel.html
为什么要用时间轮实现
- 通常用于实现linux内核任务、游戏类的buf计时。
- 单个时间轮局限性:保存的任务数量少,不能超过当前时间轮。
- 多层时间轮,典型:日-时-分-秒
- 传统java实现定时:Timer,只能单线程,会阻塞; Executors.newScheduledThreadPoll, 使用的最小堆来实现,任务还是不能太多,添加时间复杂度为O(logn)
时间轮简单实现
1.实现日、时、分、秒、毫秒时间轮,设置Tick间隔,tick=100ms
2.创建对应的时间轮数组,ms=1000/tick,s=60,min=60,hour=24,day=365,数组中的每个元素类型为List
3.添加定时任务(不能添加到当前tick区间)
3.1 获取当前时间轮时间 curTime, 获取定时任务delayTime
3.2使用curTime+delayTime获得任务下一次执行的事件,并判断是否需要时间轮升级(ms->s->min->hour->day)
3.3将该定时任务添加到升级后时间轮的具体索引上
4.时间轮-one Tick增加
4.1 每一个tick间隔,将会进行一次one-tick增加,会触发新的tick间隔内的任务,如果该任务period大于0,则继续添加该定时任务,参考3
4.2 每一层的时间轮完成后都会进行,时间轮升级(例如59s,增加到下一轮,现获取下一轮min的所有任务,然后再更新新的min对应s的时间轮),升级后新的时间轮任务就会在具体的降级时间轮中进行定位添加;
时间轮算法实现-参考https://github.com/wolaiye1010/zdc-java-script/ 加了些注释,增加取消
public class TimeWheelService {
public static void main(String[] args) {
TimeWheelService timeWheelService = new TimeWheelService(3);
timeWheelService.schedule(()->{
for(int i=0;i<100;i++){
final int a=i;
timeWheelService.schedule(()-> System.out.println("^^^^^^buff-"+a),100,80);
}
},100,0);
timeWheelService.schedule(()->{
for(int i=0;i<100;i++){
final int a=i;
timeWheelService.schedule(()-> System.out.println("=====>debuff-"+a),100,100);
}
},100,0);
timeWheelService.schedule(()-> System.out.println(new Date()),10,1000);
}
private MultiTimeWheel timeWheel=new MultiTimeWheel();
private TimeWheelThread timeWheelThread=new TimeWheelThread();
//每轮的时间轮长度
private static final int TICK=10;
private static final int wheelIndexMillisecondLength=1000/TICK;
private static final int wheelIndexSecondLength=60;
private static final int wheelIndexMinuteLength=60;
private static final int wheelIndexHourLength=24;
private static final int wheelIndexDayLength=365;
//每一轮对应的所有ticks
private static final long wheelMillisecondAllTicks=1L;
//1s 10格
private static final long wheelSecondAllTicks=wheelMillisecondAllTicks*wheelIndexMillisecondLength;
//1min 600
private static final long wheelMinuteAllTicks=wheelSecondAllTicks*wheelIndexSecondLength;
//1h
private static final long wheelHourAllTicks=wheelMinuteAllTicks*wheelIndexMinuteLength;
//1day
private static final long wheelDayAllTicks=wheelHourAllTicks*wheelIndexHourLength;
//每一轮当前的索引,可以精确获取时间
private AtomicInteger wheelIndexMillisecond=new AtomicInteger(0);
private AtomicInteger wheelIndexSecond=new AtomicInteger(0);
private AtomicInteger wheelIndexMinute=new AtomicInteger(0);
private AtomicInteger wheelIndexHour=new AtomicInteger(0);
private AtomicInteger wheelIndexDay=new AtomicInteger(0);
//实际存储
private volatile Vector[] wheelMillisecond=new Vector[wheelIndexMillisecondLength];
private volatile Vector[] wheelSecond=new Vector[wheelIndexSecondLength];
private volatile Vector[] wheelMinute=new Vector[wheelIndexMinuteLength];
private volatile Vector[] wheelHour=new Vector[wheelIndexHourLength];
private volatile Vector[] wheelDay =new Vector[wheelIndexDayLength];
public void schedule(Runnable runnable,long delay,long period){
if(period<TICK && period>0) throw new RuntimeException("不能使得间隔周期小于时间片TICK:"+TICK+" ms,间隔周期可以为0ms");
synchronized(this){
TimeWheelTask timeWheelTask = new TimeWheelTask(delay, period, runnable);
schedule(timeWheelTask);
}
}
public void schedule(TimeWheelTask timeWheelTask){
//delay 加上当前相对于具体时间单位的余数。
//处理当前是0分59s时加入了1分1秒后任务,会导致在1分1秒时候执行,因此如果延迟本身大于当前的一轮周期,则用延迟加上当前时间与本轮毫秒值的余数
//00:00:59 + 61 = 00:02:00,可知,需要先加上本轮余数
timeWheelTask.delay=timeWheelTask.delay+timeWheel.getWheelNowTime(timeWheelTask.delay);
timeWheel.addTaskToWheel(timeWheelTask.delay,timeWheelTask);
}
//真正执行定时任务的线程池
private ThreadPoolExecutor threadPoolExecutor;
public TimeWheelService(int coreSize){
this.threadPoolExecutor=new ThreadPoolExecutor(coreSize,coreSize,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(10000));
timeWheelThread.start();
}
//轮子
class MultiTimeWheel{
/**
* 增加 one-tick,可能会触发每层轮,时间轮的升级
*/
public void incrTick() {
if(incIndex(TimeUnit.MILLISECONDS)){
return;
}
if(incIndex(TimeUnit.SECONDS)){
return;
}
if(incIndex(TimeUnit.MINUTES)){
return;
}
if(incIndex(TimeUnit.HOURS)){
return;
}
incIndex(TimeUnit.DAYS);
}
//增加一个tick,处理因为升级导致的新事件添加
private boolean incIndex(TimeUnit timeUnit){
long allTicksNext;
Vector[] vectorsNext;
AtomicInteger index;
AtomicInteger indexNext;
int wheelLength;
int wheelLengthNext;
switch(timeUnit){
case DAYS:
allTicksNext=0;
vectorsNext=null;
index=wheelIndexDay;
indexNext=null;
wheelLength=wheelIndexDayLength;
wheelLengthNext=0;
break;
case HOURS:
allTicksNext=wheelDayAllTicks;
vectorsNext=wheelDay;
index=wheelIndexHour;
indexNext=wheelIndexDay;
wheelLength=wheelIndexHourLength;
wheelLengthNext=wheelIndexDayLength;
break;
case MINUTES:
allTicksNext=wheelHourAllTicks;
vectorsNext=wheelHour;
index=wheelIndexMinute;
indexNext=wheelIndexHour;
wheelLength=wheelIndexMinuteLength;
wheelLengthNext=wheelIndexHourLength;
break;
case SECONDS:
allTicksNext=wheelMinuteAllTicks;
vectorsNext=wheelMinute;
index=wheelIndexSecond;
indexNext=wheelIndexMinute;
wheelLength=wheelIndexSecondLength;
wheelLengthNext=wheelIndexMinuteLength;
break;
case MILLISECONDS:
allTicksNext=wheelSecondAllTicks;
vectorsNext=wheelSecond;
index=wheelIndexMillisecond;
indexNext=wheelIndexSecond;
wheelLength=wheelIndexMillisecondLength;
wheelLengthNext=wheelIndexSecondLength;
break;
default:
throw new RuntimeException("Timeunit 参数错误");
}
index.getAndIncrement();
if(index.get()<wheelLength){
return true;
}
index.set(index.get()%wheelLength);
//如果是天数,因为当处理hours时候已经处理过天了,所以直接返回。
if(timeUnit.equals(TimeUnit.DAYS)){
return true;
}
//获取下一个时间轮的任务,并添加
List<TimeWheelTask> taskList = vectorsNext[(indexNext.get() + 1) % wheelLengthNext];
if(null!=taskList){
for(TimeWheelTask task:taskList){
addTaskToWheel(task.delay%(allTicksNext),task);
}
taskList.clear();
}
return false;
}
public List<TimeWheelTask> getTaskList() {
return wheelMillisecond[wheelIndexMillisecond.get()];
}
//加入时间轮,判断是否需要升级
void addTaskToWheel(long delay,TimeWheelTask task){
if(delay>=wheelIndexDayLength*wheelDayAllTicks*TICK){
throw new RuntimeException("不能超过一年");
}
if(addTaskToWheel(delay,task,TimeUnit.DAYS)){
return;
}
if(addTaskToWheel(delay,task,TimeUnit.HOURS)){
return;
}
if(addTaskToWheel(delay,task,TimeUnit.MINUTES)){
return;
}
if(addTaskToWheel(delay,task,TimeUnit.SECONDS)){
return;
}
addTaskToWheel(delay,task,TimeUnit.MILLISECONDS);
}
//添加任务到时间轮,
private boolean addTaskToWheel(long delay, TimeWheelTask timeWheelTask, TimeUnit timeUnit){
long allTicks;
Vector[] vectors;
AtomicInteger index;
int wheelLength;
switch (timeUnit){
case DAYS:
allTicks=wheelDayAllTicks;
vectors= wheelDay;
index=wheelIndexDay;
wheelLength=wheelIndexDayLength;
break;
case HOURS:
allTicks=wheelHourAllTicks;
vectors=wheelHour;
index=wheelIndexHour;
wheelLength=wheelIndexHourLength;
break;
case MINUTES:
allTicks=wheelMinuteAllTicks;
vectors=wheelMinute;
index=wheelIndexMinute;
wheelLength=wheelIndexMinuteLength;
break;
case SECONDS:
allTicks=wheelSecondAllTicks;
vectors=wheelSecond;
index=wheelIndexSecond;
wheelLength=wheelIndexSecondLength;
break;
case MILLISECONDS:
allTicks=wheelMillisecondAllTicks;
vectors=wheelMillisecond;
index=wheelIndexMillisecond;
wheelLength=wheelIndexMillisecondLength;
break;
default:
throw new RuntimeException("timeUnit 参数错误");
}
//添加到当前的索引
if(0!=delay/(allTicks*TICK) || timeUnit.equals(TimeUnit.MILLISECONDS)){
int indexNew=(index.get()+(int)(delay/(allTicks*TICK)))%wheelLength;
if(null==vectors[indexNew]){
vectors[indexNew]=new Vector();
}
vectors[indexNew].add(timeWheelTask);
return true;
}
return false;
}
//准确获取当前需要添加的准确时间轮
long getWheelNowTime(long delay){
//当前时间 毫秒
long timeFromWheelStart=(wheelIndexDay.get()*wheelDayAllTicks+wheelIndexHour.get()*wheelHourAllTicks+wheelIndexMinute.get()*wheelMinuteAllTicks
+wheelIndexSecond.get()*wheelSecondAllTicks+wheelIndexMillisecond.get()*wheelMillisecondAllTicks)*TICK;
//从大到小开始处理,是否大于1天
if(0!=delay/(wheelDayAllTicks*TICK)){
return timeFromWheelStart%(wheelDayAllTicks*TICK);
}
//大于1小时
if(0!=delay/(wheelHourAllTicks*TICK)){
return timeFromWheelStart%(wheelHourAllTicks*TICK);
}
//大于1分钟
if(0!=delay/(wheelMinuteAllTicks*TICK)){
return timeFromWheelStart%(wheelMinuteAllTicks*TICK);
}
//大于1秒
if(0!=delay/(wheelSecondAllTicks*TICK)){
return timeFromWheelStart%(wheelSecondAllTicks*TICK);
}
return 0;
}
}
/**
* 时间轮的task
*/
class TimeWheelTask implements Runnable{
private long delay;
//-1 为手动取消、0为1次定时、大于0表示正常调度间隔
private long period;
private Runnable runnable;
public void setDelay(long delay) {
this.delay = delay;
}
TimeWheelTask(long delay, long period, Runnable runnable){
this.delay=delay;
this.period=period;
this.runnable=runnable;
}
/**
* 判断是否是周期性的调度任务
* @return
*/
public boolean isPeriodSchedule(){
return period>0;
}
@Override
public void run() {
if(this.period==-1){
return;
}
runnable.run();
}
/**
* 手动消除调度
*/
public void cancel(){
this.period=-1;
}
}
//时间轮 main线程
class TimeWheelThread extends Thread{
public TimeWheelThread(){
super("TimeWheel_main");
}
public TimeWheelThread(String thread_name){
super(thread_name);
}
@Override
public void run() {
try {
mainLoop();
}catch (Exception e){
System.out.println(e);
}finally {
System.out.println(111);
}
}
private void mainLoop() {
while (true){
//运行任务
runTask(timeWheel.getTaskList());
//时间增加 one-tick
timeWheel.incrTick();
//休眠
try {
TimeUnit.MILLISECONDS.sleep(TICK);
} catch (InterruptedException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
private void runTask(List<TimeWheelTask> taskList) {
if(taskList==null || taskList.size()==0) return;
for(TimeWheelTask timeWheelTask:taskList){
threadPoolExecutor.execute(timeWheelTask);
if(timeWheelTask.isPeriodSchedule()){
timeWheelTask.setDelay(timeWheelTask.period);
schedule(timeWheelTask);
}
}
taskList.clear();
}
}
}
配合delayQueue实现空间换时间,对时间轮进行推进。参考:https://github.com/anurnomeru/Solution.git
角色
Bucket:槽位:链式环形存储任务
Timer:主循环:推进时间轮、持有工作线程池(实际执行任务的)、主循环线程池
Timewheel: 时间轮:(持有父级时间轮引用),添加任务、时间Tick(间隔)、持有Bucket size,当前时间
TimeTask: 运行任务:作为Bucket 持有的任务引用、失效时间、取消状态等,
- 创建DelayQueue, TimeWheel以及附属(上级)时间轮持有一个queue
- 可以实现对时间轮中的槽位进行改造,实现槽内任务链式存储、槽位本身实现Delayed接口,配合DelayQueue实现时间轮推进
- 每一个任务添加时,对应的槽位判断是否添加到queue,如果时间超时超出当前时间轮范围,则获取上级时间轮(如果不存在)则创建。
- 时间轮推进则依靠queue.poll(timeout),如果有最近到期任务,就通过timewheel推进时间轮。
使用ScheduledThreadPoll
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println(new Date());
}, 10, 20, TimeUnit.SECONDS);
自有博客:https://blog.wudd.top/
那一天我二十一岁,在我一生的黄金时代。我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云。后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消失,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。