• 定时器与系统时间


    问题:
    --------------------------------------------------------------------------------
    用户反馈一些定时活动提前开启或者延后开启
    1) 登录服务器,查看时间确实慢了或者快了。总之是有几台服务器时间不准确了。
    2) 查看代码是使用的ScheduledExecutorService.scheduleAtFixedRate,Java的API,不至于这里存在Bug
    3) 查看log4j日志输出发现:
        12点的定时活动,之前的[活动运行时间]就是12点整;后面有几天的[活动运行时间]是12点零几分,而且分秒都一致
        确认了一下,变化之间同步了一下服务器时间,但是没有重启jvm
    4) 初步怀疑是ScheduledExecutorService内部执行使用的是相对时间,不是每次采样服务器系统时间

    问题确认-测试:
    --------------------------------------------------------------------------------
    5) 测试

    ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
    service.scheduleAtFixedRate(new Runnable() { // Runnable-1
        @Override
        public void run() {
            System.out.println( String.format("
    #### %s ####", 
                    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S").format(new Date())));
        }
    }, 0, 10, TimeUnit.SECONDS); // 10秒执行一次
    service.scheduleAtFixedRate(new Runnable() { // Runnable-2
        int i = 0;
        @Override
        public void run() {
            System.out.print( (i++) + "," );
        }
    }, 0, 1, TimeUnit.SECONDS);    // 1秒执行一次

    输出-1:
        0,
        #### 2014-11-28 16:51:48.118 ####
        1,2,3,4,5,6,7,8,9,
        #### 2014-11-28 16:51:58.93 ####
        10,11,12,13,14,15,16,17,18,19,20,
        #### 2014-11-28 16:52:08.94 ####
        21,22,23,24,25,26,27,28,29,
        #### 2014-11-28 16:52:18.93 ####
        30,31,32,33,34,35,36,37,38,39,
        #### 2014-11-28 16:52:28.93 ####
        40,41,42,43,44,45,46,47,48,49,
        #### 2014-11-28 16:58:36.480 #### // 调整时间
        50,51,52,53,54,55,56,57,58,59,
        #### 2014-11-28 16:58:46.480 ####
        60,61,62,63,64,65,66,67,68,69,
        #### 2014-11-28 16:58:56.480 ####

    在 16:52:28.93 时调整时间为16:58:36.480(向后跳), Runnable-2依然进行了10次输出,然后Runnable-1输出1次

    输出-2:
        0,
        #### 2014-11-28 17:12:40.971 ####
        1,2,3,4,5,6,7,8,9,
        #### 2014-11-28 17:12:50.943 ####
        10,11,12,13,14,15,16,17,18,19,
        #### 2014-11-28 17:13:00.943 #### // 调整时间
        20,21,22,23,24,25,26,27,28,29,
        #### 2014-11-28 17:05:09.69 ####
        30,31,32,33,34,35,36,37,38,39,
        #### 2014-11-28 17:05:19.68 ####

    在 17:13:00.943 时调整时间为17:05:09.69(向前跳), Runnable-2依然进行了10次输出,然后Runnable-1输出1次

    测试结论: 时间的跳跃不影响 Runnable-1 10个秒单位输出一次, ScheduledExecutorService 没有使用系统时间

    问题确认-JDK源码:
    --------------------------------------------------------------------------------
    初始化

    ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        new ScheduledThreadPoolExecutor(corePoolSize)
            super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new DelayedWorkQueue())

    注册定时任务

    service.scheduleAtFixedRate(new Runnable() {...})
        RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Object>(command, null, 
                triggerTime(initialDelay, unit), unit.toNanos(period)));    // ScheduledThreadPoolExecutor.ScheduledFutureTask
        delayedExecute(t);
            prestartCoreThread
                addIfUnderCorePoolSize
                    addThread
                        Worker w = new Worker(firstTask);   // ThreadPoolExecutor.Worker
                        Thread t = threadFactory.newThread(w);
                        workers.add(w);
            super.getQueue().add(command);      // DelayedWorkQueue

    执行

    ThreadPoolExecutor.Worker.run
        task = getTask()
            r = workQueue.take();       // DelayedWorkQueue
    
    ScheduledThreadPoolExecutor.DelayedWorkQueue.take
        dq.take();         // DelayQueue<RunnableScheduledFuture>
    DelayQueue<E extends Delayed>
    Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。 该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。 如果延迟都还没有期满,则队列没有头部,并且 poll 将返回
    null。 当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。 即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。 例如,size 方法同时返回到期和未到期元素的计数。此队列不允许使用 null 元素。 take() long delay = first.getDelay(TimeUnit.NANOSECONDS); if (delay > 0) { long tl = available.awaitNanos(delay); }
    ScheduledThreadPoolExecutor.ScheduledFutureTask.getDelay  
    public
    long getDelay(TimeUnit unit) { return unit.convert(time - now(), TimeUnit.NANOSECONDS); } final long now() { /** * public static long nanoTime() * 返回最准确的可用系统计时器的当前值,以毫微秒为单位。 * 此方法只能用于测量已过的时间,与系统或钟表时间的其他任何时间概念无关。 * 返回值表示从某一固定但任意的时间算起的毫微秒数(或许从以后算起,所以该值可能为负)。 */ return System.nanoTime() - NANO_ORIGIN; }

    问题确认-nanoTime测试:
    --------------------------------------------------------------------------------

    new Thread(){
        public void run () {
            long lastNanos = System.nanoTime();
            long lastMilis = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) {
                try { Thread.sleep(10000L); } catch (InterruptedException e) { } // 10秒钟输出一次
                
                long nanos = System.nanoTime();
                long millis = System.currentTimeMillis();
                
                System.out.println( String.format("%-25s %-10s %s-%s = %s [%s]",
                        new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S").format(new Date(millis)), 
                        millis - lastMilis,
                        nanos, lastNanos,
                        nanos - lastNanos, (nanos - lastNanos) / 1000 / 1000
                ));
                
                lastNanos = nanos;
                lastMilis = millis;
            }
        }
    }.start();

    输出:
        2014-12-05 10:10:45.950   10001        107273041687944-107263041775958 = 9999911986 [9999]
        2014-12-05 10:10:55.997   10047        107283088127723-107273041687944 = 10046439779 [10046]
        2014-12-05 10:20:04.198   548201      107293088578057-107283088127723 = 10000450334 [10000]
        2014-12-05 10:00:12.399   -1191799   107303089035855-107293088578057 = 10000457798 [10000]
        2014-12-05 10:00:22.399   10000        107313089495208-107303089035855 = 10000459353 [10000]
        在 10:10:55.997 时调整时间为10:20:04.198(向后跳), 在 10:20:04.198 时调整时间为10:00:12.399(向前跳),
    nanos - lastNanos总是维持在 10s左右, millis - lastMilis 确实期望中的时间

    想法与目标:
    --------------------------------------------------------------------------------
    7) 我想做一个定时器,依赖于当前系统时间的定时器;顺便解决当前问题
        a) 它不是定时任务,不是一个任务系统。
        b) 它只做一件事情,到时间了提醒我做某个任务。
        比如,12点了,提醒我该吃叫花鸡了;20点了,该打帮会战了。

    初步设计:
    --------------------------------------------------------------------------------

    8) 初步设计
        a) 首先创建一个任务线程池,用于执行定时器叫醒的任务。
            Executors.newScheduledThreadPool(workServicePoolSize)
        b) 创建一个定时器线程, 每隔1秒执行一次handleFunc(心跳步长1秒)
            Executors.newSingleThreadScheduledExecutor
        c) handleFunc根据当前系统时间查找到时的任务,把任务放置到任务线程池,由任务线程池执行
            execute(new Runnable(){});
        可以初步解决任务依赖系统时间来执行问题

    详细设计:
    --------------------------------------------------------------------------------

    9) Linux系统时间会变快或者变慢,比如23点战力排行榜截止并在30分钟后开始领取奖励
        a) 如果快的太多;大家当前都是22:55,但是服务器已经23:00,想等着最后冲榜的兄弟立马就哭了
        b) 如果慢的太多;大家当前都是23:00,但是服务器才是22:55,我都休息了,准备开始领取奖励,你还能冲击战力榜

        a) handleFunc每次执行后,记录一下当前执行时间为 lastExecuteTime
        b) handleFunc下次执行的时候,拿 executeTime(当前时间) 和 lastExecuteTime比较一下
            如果 executeTime == lastExecuteTime + 1: (心跳步长1秒)
                正常时间,正常执行
            如果 executeTime > lastExecuteTime + 1:
                时间快了(通常是服务器时间慢了;校正服务器时间,服务器时间会快进), 需要处理一下:
                    [lastExecuteTime + 1, executeTime - 1]的任务 根据业务决定是否需要立马补执行
                    [executeTime]的任务 是当前时间正常任务,需要正常执行
            如果 executeTime <= lastExecuteTime:
                时间慢了(通常是服务器时间快了;校正服务器时间,服务器时间会回退):
                    [executeTime, lastExecuteTime] 都执行过了,一般不需要再执行了

        注1:当前时间和lastExecuteTime都是抹去毫秒的,日常定时服务基于秒来计算足够了
        注2: 服务器可以每隔1个小时同步一次时间,比方 NN:38,通常要避开整点、半点、整十分
        注3: 每小时同步一次最多误差几秒而已,对于普通业务而言:
            时间快了的情况下,立马补执行一下就可以了,比较重要的奖励提前或者延迟5秒发没有多少差别
            时间慢了的情况下,可以忽视掉[executeTime, lastExecuteTime]间的任务,不需要再执行一次了
        注4: 执行时间粒度比较小的,比方说1秒执行一次的,可以无视时间跳跃的问题

    详细设计-定时任务:
    --------------------------------------------------------------------------------

    10) 这样用定时器的方案,可以解决时间跳跃的问题;但是日常开发通常是定时周期任务
        比如, 12点吃叫花鸡,12点定时器通知吃叫花鸡;但是吃叫花鸡是每天12点都吃,这就是个定时周期任务,需要每天12点都
        通知一下吃。处理方案可以如下:
        a) 修改“handleFunc根据当前系统时间查找到时的任务”
        b) 查找的任务仓库分为两类仓库:
            一次性任务仓库:
                到时间就执行任务,并移除; 任务仓库的存储的key是任务执行时间戳
            每日的任务仓库:
                任务仓库的存储的key是,任务相对于凌晨00:00:00的秒数
                handleFunc执行时候,查看一下,今天过去了多少秒(这个是使用系统时间),找到对应任务,然后执行。这个不需要移除任务

        这样依然是定时器的概念,“12点到时间了,我叫你去吃叫花鸡;明天12点到时间了,我再叫你去吃叫花鸡”,
        而不是“12点了,我叫你去吃叫花鸡;24小时后我再叫你去吃鸡”。

        注1: 同理可以增加每周、每小时、每分钟的任务仓库
        注2: 通常不需要每月、每年、每十年等周期任务,如果需要加也很简单
        注3: 每秒的,不需要这么多少事情, handleFunc 过了就直接执行(每次时间间隔是利用nano计算出来的1秒,所以应该执行)

    接口设计:
    --------------------------------------------------------------------------------

    11) 对外接口:
        a) 启动
        b) 关闭
        c) 注册一次性任务
        d) 注册每周任务
        e) 注册每日任务
        f) 注册每时任务:每个小时的第几分、第几秒执行的什么任务
        g) 注册每分任务
        h) 注册每秒任务

    其它说明:
    --------------------------------------------------------------------------------

    12) 其它:
        a) handleFunc 只是找到任务把它扔进工作线程池执行,不怎么占用CPU,不会造成任务选取的堵塞
        b) handleFunc 每次都会去 分钟的任务仓库查找合适的任务并执行;同一任务上一分钟没执行玩,当前任务也会继续执行,不会延迟,会同时执行
            既然是每分钟任务,任务不应该超过1分钟;如果偶尔会超过1分钟,可以在注册的任务里面自行加锁
        c) 服务器时间慢了;校正服务器时间,服务器时间会快进,补执行任务只补处理一定时间(比如30分钟)
            系统时间是1小时同步一次,误差最大不过几秒;如果再大,就应该升级内核或者换服务器了
            补执行的时间段过程,可能会影响正常服务(正常服务进程、系统资源占用等等)
        d) 任务扔到线程池里面时候,会额外catch住,防止挂掉当前线程

  • 相关阅读:
    解决is not a supported wheel on this platform-解决pip has no attribute pep425tags-解决网上旧教程不适用的问题
    语音信号实验1-基于时域分析技术的语音识别
    解决 ModuleNotFoundError: No module named 'pip'
    Typecho如何上传模板和插件
    获取图片的URL链接(Typecho修改背景图片等都需要URL链接)
    ubuntu云服务器安装anaconda+jupyter配置
    ORACLE用户密码过期啦
    关于Nacos启动报如下错:nacos no javac in (usr/lib/jvm-1.8.0)
    记录一次华为云服务器给根目录扩容
    记录一次NFS快照es集群索引备份
  • 原文地址:https://www.cnblogs.com/lixingxing/p/4188695.html
Copyright © 2020-2023  润新知