【原】定时器与系统时间

问题:
--------------------------------------------------------------------------------
用户反馈一些定时活动提前开启或者延后开启
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("\n#### %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住,防止挂掉当前线程

时间: 2024-11-03 21:04:17

【原】定时器与系统时间的相关文章

【原】定时器与系统时间(续)

额外问题处理: -------------------------------------------------------------------------------- 13) 定时器之外的一些处理     a) window环境下,定时器通知执行定时任务的时间点可能误差1毫秒:Linux环境也有类似情况,但是误差频率低很多(只是在指定时间点前后1毫秒误差,心跳步长不差)         可能的影响:             以每秒任务为例, 10:00:00.000 TaskA ->

标准IO的简单应用,动静态库,读取系统时间并打印,模拟ls -l功能

2015.2.27星期五,小雨 标准IO实现的复制功能: #include <stdio.h>#include <errno.h> #define N 64 int main(int argc, char *argv[]){ int n; char buf[N]; FILE *fps, *fpd; if(argc < 3) { printf("usage : %s <src_file><dst_file>\n",argv[0]);

C语言获取系统时间的几种方式

C语言获取系统时间的几种方式 2009-07-22 11:18:50|  分类: 编程学习 |字号 订阅 C语言中如何获取时间?精度如何? 1 使用time_t time( time_t * timer ) 精确到秒2 使用clock_t clock() 得到的是CPU时间 精确到1/CLOCKS_PER_SEC秒3 计算时间差使用double difftime( time_t timer1, time_t timer0 )4 使用DWORD GetTickCount() 精确到毫秒5 如果使用

[2014.5.22][UBUNTU]Ubuntu与Windows系统时间不同步的问题

安装Ubuntu+Windows双系统时会遇到Windows和Ubuntu系统时间不同步的问题,这是因为Windows系统默认读取主板bios等硬件系统时间作为OS的当地时间;而MAc,Linux类的OS以主板bios等硬件系统时间作为UTC时间,然后操作系统的时间以UTC为标准按照使用者的时区设置加加减减.这就造成了中国用户会出现一个很有趣的现象:Ubuntu上的时间会比windows上的快8小时(至于是怎么算的 请童鞋自己掰手指头;是不是寓意Ubuntu快windows一筹?). 解决这个问

wpf 窗体中显示当前系统时间

先看一下效果: 这其实是我放置了两个TextBlock,上面显示当前的日期,下面显示时间. 接下来展示一下代码: 在XAML中: <StackPanel Width="205"                    Margin="0,0,57,0"                    HorizontalAlignment="Right">            <TextBlock Height="Auto&qu

关于时间的操作(JavaScript版)——年月日三级联动(默认显示系统时间)

这个功能是大学时自己使用纯JavaScript写的,没有借助Jquery,呵呵呵,看起来有点繁琐,可是在当时依稀的记得功能实现后自己好好的高兴一把了呢,从现在来看那时候的自己是多么的幼稚.多么的无知: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>年月日三级联动(默认显示系统时间)</title> <

MFC在static text中动态显示系统时间

1.添加static text ,ID为IDC_ShowTime 2,在OnInitDialog()中 SetTimer(1,1000,NULL);         //启动定时器 3.添加WM_TIMER消息处理函数,ontimer中添加如下代码: CString strTime; CTime tm; tm=CTime::GetCurrentTime(); strTime=tm.Format("%Y-%m-%d %H:%M:%S"); SetDlgItemText(IDC_ShowT

[转载]如何使Android应用程序获取系统权限来修改系统时间

在 android 的API中有提供 SystemClock.setCurrentTimeMillis()函数来修改系统时间,可惜无论你怎么调用这个函数都是没用的,无论模拟器还是真机,在logcat中总会得到"Unable to open alarm driver: Permission denied ".这个函数需要root权限或者运行与系统进程中才可以用. 本来以为就没有办法在应用程序这一层改系统时间了,后来在网上搜了好久,知道这个目的还是可以达到的. 第一个方法简单点,不过需要在

解决weblogic与系统时间相差8小时的问题

解决weblogic与系统时间相差8小时的问题 在一般情况下weblogic与系统时间是很少会出现时间差的问题,但有可能在某一特定的情况下就会出现,如使用weblogic8版本时可能会出现时差问题: 调整时差方法: 第一步: 1.找到D:\bea\jdk142_08\jre\lib\zi\Etc和D:\bea\jrockit81sp5_142_08\jre\lib\zi\Etc找到这两个文件夹里的Etc这个文件夹,里面就是时区文件, 第二步: 2.将GMT-8这个文件复制一份然后重命名为GMT来