ScheduledThreadPoolExecutor与System#nanoTime

一直流传着Timer使用的是绝对时间,ScheduledThreadPoolExecutor使用的是相对时间,那么ScheduledThreadPoolExecutor是如何实现相对时间的?

先看看ScheduledThreadPoolExecutor中实现定时调度的模型,很简单,内部用了无界的DelayQueue作为线程池的队列,而DelayQueue的内部又使用的是一个PriorityQueue,那么,最先需要定时调度的任务位于队首。定时任务实现逻辑大概如此:创建ScheduledThreadPoolExecutor对象的时候会记录一个常量值t,定时任务中有一个以t为基础的多久以后会被执行的属性,在线程拿到队首任务(可能等待了一段时间)执行后,会修改这个属性为下一次要执行的基于t的时间量,然后将其再放入队列中。整个逻辑都在任务的run方法中:

public void run() {
     if (isPeriodic())
         runPeriodic();
     else
         ScheduledFutureTask. super .run();
}

如果是周期性任务,会执行runPeriodic:

private void runPeriodic() {
     boolean ok = ScheduledFutureTask. super .runAndReset();
     boolean down = isShutdown();
     // Reschedule if not cancelled and not shutdown or policy allows
     if (ok && (!down ||
                (getContinueExistingPeriodicTasksAfterShutdownPolicy() &&
                 !isTerminating()))) {
         long p = period;
         if (p > 0 )
             time += p;
         else
             time = triggerTime(-p);
         ScheduledThreadPoolExecutor. super .getQueue().add( this );
     }
     // This might have been the final executed delayed
     // task.  Wake up threads to check.
     else if (down)
         interruptIdleWorkers();
}

下面这段代码就是周期性任务实现的逻辑:

if (p > 0 )
     time += p;
else
     time = triggerTime(-p);
ScheduledThreadPoolExecutor. super .getQueue().add( this );

重新回到相对时间问题,首先看看DelayQueue的take方法:

public E take() throws InterruptedException {
     final ReentrantLock lock = this .lock;
     lock.lockInterruptibly();
     try {
         for (;;) {
             E first = q.peek();
             if (first == null ) {
                 available.await();
             } else {
                 long delay =  first.getDelay(TimeUnit.NANOSECONDS);
                 if (delay > 0 ) {
                     long tl = available.awaitNanos(delay);
                 } else {
                     E x = q.poll();
                     assert x != null ;
                     if (q.size() != 0 )
                         available.signalAll(); // wake up other takers
                     return x;
 
                 }
             }
         }
     } finally {
         lock.unlock();
     }
}

拿到队列里的第一个元素(也就是最先需要执行的),但并不删除,获取该元素的等待时间(也就是getDelay),然后不断的awaitNanos。如果此时加入了一个比这个队首元素还要先执行的任务会怎样?看下add和offer方法的实现(add方法就是直接调用的offer):

public boolean offer(E o) {
     final ReentrantLock lock = this .lock;
     lock.lock();
     try {
         E first = q.peek();
         q.offer(o);
         if (first == null || o.compareTo(first) < 0 )
             available.signalAll();
         return true ;
     } finally {
         lock.unlock();
     }
}

变量q即是一个PriorityQueue,如果o对象表示的任务比目前队首的任务更先执行,那么q.offer(o)会将o弄到队首,在这种情况下o.compareTo(first)是小于0的,因此会通知在available上等待的线程。而在take方法里,线程一直在available上awaitNanos,此时若被唤醒,它就会继续循环,重新拿到队列的第一个元素,也就是新加入的元素并重复之前的过程,这样,最先需要调度的任务就永远排在第一位。

回到take方法的long delay = first.getDelay(TimeUnit.NANOSECONDS)这一句,在ScheduledThreadPoolExecutor实现中,这个first变量的类型就是ScheduledThreadPoolExecutor.ScheduledFutureTask,这是ScheduledThreadPoolExecutor中的一个私有类,看看其getDelay方法的实现:

public long getDelay(TimeUnit unit) {
     return  unit.convert(time - now(), TimeUnit.NANOSECONDS);
}

其中的now方法:

final long now() {
     return System.nanoTime() - NANO_ORIGIN;
}

刚开始看到这个代码时,心想这何以实现相对时间。潜意识中对nanoTime认识一直是这样的:返回值是纳秒,与currentTimeMillis()一样返回的是与协调世界时 1970 年 1 月 1 日午夜之间的时间差。经过测试与查看API文档,原来对nanoTime()的认识一直是错误的。

API中关于nanoTime是这么描述的:

返回最准确的可用系统计时器的当前值,以毫微秒为单位。 
此方法只能用于测量已过的时间,与系统或钟表时间的其他任何时间概念无关。返回值表示从某一固定但任意的时间算起的毫微秒数(或许从以后算起,所以该值可能为负)。此方法提供毫微秒的精度,但不是必要的毫微秒的准确度。它对于值的更改频率没有作出保证。在取值范围大于约 292 年(263 毫微秒)的连续调用的不同点在于:由于数字溢出,将无法准确计算已过的时间。 

返回值的单位为毫微秒不是该方法的重点,重点在于“与系统或钟表时间的其他任何时间概念无关。返回值表示从某一固定但任意的时间算起的毫微秒数”,也就是说,它的返回值是一个相对“某一固定但任意的时间”的偏移量,而不依赖于系统时钟是否改变,也无法通过这个方法的返回值计算当前日期。而这个相对性正是ScheduledThreadPoolExecutor所需要的。

很多计算代码运行耗时的地方使用了currentTimeMillis(),那么在系统时间变动的那一刻(如NTP时间同步),耗时计算结果是不准确的,尤其是时间变动较大时,如果在日志中发现某个调用突然耗时很大,还以为出现什么问题了。

关于nanoTime有些有趣的问题,用该方法计算运行耗时得出的结果竟然会是负数:http://stackoverflow.com/questions/510462/is-system-nanotime-completely-useless,只是看到这个话题,我的系统上(xp sp3)没有重现。

最后,题外话,关于系统时间:尽可能不要在生产代码中使用Thread#sleep,因为“此操作受到系统计时器和调度程序精度和准确性的影响”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值