时间轮算法 简单学习

时间轮

参考: 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);
posted @ 2020-08-17 22:46  bendandan  阅读(930)  评论(0编辑  收藏  举报