首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》6.4 一个来自于硬件的时间问题

关灯直达底部

你有没有想过计算机里的时间存在哪里以及在哪里处理?我们都知道硬件最终负责跟踪时间,但事实可能不像你想的那么简单。

为了进行性能调优,你需要对时间如何工作有深刻的认识。为此我们先从底层硬件开始讨论,然后探讨Java如何与这些子系统集成,最后介绍nanoTime方法的复杂性。

6.4.1 硬件时钟

在基于x64的机器里有四种不同的硬件时间源:RTC、8254、TSC以及HPET。

实时时钟(RTC)基本上和便宜的电子表(基于石英晶体)里找到的电子器件一样,在系统断电时由主板上的电池供电。系统在启动时就是从它那里得到时间的,不过很多机器在OS启动过程中会通过网络时间协议(Network Time Protocol,NTP)跟网络上的时间服务器同步。

所有古董都曾是新东西

实时时钟这个名字现在看来十分不恰当——在20世纪80年代它刚出现时确实被认为是实时的,但现在它的准确度对于关键应用来说已经不够用了。以“新”或“快”命名的创新经常是这种结局,比如巴黎的Pont Neuf(“新桥”)。它建于1607年,现在已经是巴黎市内最古老的桥了。

8254是可编程计时芯片,也是始祖级的东西。它的时钟源是一个119.318kHz的晶体,这个频率是NTSC彩色副载波频率的三分之一,这也是它返回到CGA图形系统的原因。它曾经为OS调度器提供定期时点(用于时间片),但现在已经有其他时间源(或者不再需要)了。

下面介绍应用最广泛的现代计时器——时间戳计时器(TSC)。基本上,这是一个跟踪CPU运行了多少个周期的CPU计数器。乍看起来它似乎很适合做时钟。但这个计数器是跟CPU的,并且在运行时可能会受到节能或其他因素的影响。也就是说,不同的CPU会互相偏离,也不能跟钟表时间保持一致。

最后还有高精度事件计时器(HPET)。这种计时器是最近几年才出现的,有助于人们用较老的时钟硬件更好地计时。HPET使用至少10MHz的计时器,所以其精度至少应该是1μs——但它并不是在所有硬件上都可用,也不是所有操作系统都支持。

如果这些内容看起来有点乱,那是因为它们本来就乱。好在Java平台提供了可以使用它们的工具——它把对硬件和OS支持的依赖隐藏到特定的机器配置里。然而试图隐藏依赖项的做法并没有完全成功。

6.4.2 麻烦的nanoTime

Java中有两个获取时间的方法:System.currentTimeMillisSystem.nanoTime,后面一个用于测量比毫秒更精确的时间。表6-1总结了它们两个的主要差异。

表6-1 Java内置时间获取方法的比较

currentTimeMillisnanoTime解析度为毫秒级纳秒级引用几乎所有情况下都跟钟表时间相符可能偏离钟表时间

如果表6-1中对nanoTime的描述让它看起来有点像计时器,那就对了,因为如今在大多数操作系统上,它的时间源都是CPU计数钟——TSC。

nanoTime的输出是相对于某个固定时间的。也就是说必须用它记录间隔期,用nanoTime的返回结果减去之前调用得到的返回结果。下面这段代码来自后面的一个研究案例,恰好表明了这种情况:

long t0 = System.nanoTime;doLoop1;long t1 = System.nanoTime;...long el = t1 - t0;  

eldoLoop1执行所用的时间(以纳秒为单位)。

要在性能调优中正确使用这些方法,必须对nanoTime的行为有所了解。代码清单6-1输出了毫秒计时器和纳秒计时器(通常由TSC提供)之间的最大偏离。

代码清单6-1 时间偏离

private static void runWithSpin(String args)   long nowNanos = 0, startNanos = 0;  long startMillis = System.currentTimeMillis;  long nowMillis = startMillis;  while (startMillis == nowMillis) { //将startNanos在毫秒边界上对齐    startNanos = System.nanoTime;    nowMillis = System.currentTimeMillis;    }  startMillis = nowMillis;  double maxDrift = 0;  long lastMillis;  while (true) {    lastMillis = nowMillis;    while (nowMillis - lastMillis < 1000) {      nowNanos = System.nanoTime;      nowMillis = System.currentTimeMillis;     }      long durationMillis = nowMillis - startMillis;      double driftNanos = 1000000 *   (((double)(nowNanos - startNanos)) / 1000000 - durationMillis);      if (Math.abs(driftNanos) > maxDrift) {        System.out.println(/"Now - Start = /"+ durationMillis    +/" driftNanos = /"+ driftNanos);        maxDrift = Math.abs(driftNanos);        }    }} 

这段代码会输出可观测到的最大偏离,并且证明其表现与操作系统的相关度很高。下面是Linux上的一段输出:

Now - Start = 1000 driftNanos = 14.99999996212864Now - Start = 3000 driftNanos = -86.99999989403295Now - Start = 8000 driftNanos = -89.00000011635711Now - Start = 50000 driftNanos = -92.00000204145908Now - Start = 67000 driftNanos = -96.0000033956021Now - Start = 113000 driftNanos = -98.00000407267362Now - Start = 136000 driftNanos = -98.99999713525176Now - Start = 150000 driftNanos = -101.0000123642385Now - Start = 497000 driftNanos = -2035.000012256205//注意driftNanos从-2035到20149出现了一个非常大的跳跃Now - Start = 1006000 driftNanos = 20149.99999664724Now - Start = 1219000 driftNanos = 44614.00001309812  

这里还有一个装在相同硬件上的老Solaris上的输出结果:

Now - Start = 1000 driftNanos = 65961.0000000157    //间隔很平滑Now - Start = 2000 driftNanos = 130928.0000000399Now - Start = 3000 driftNanos = 197020.9999999497Now - Start = 4000 driftNanos = 261826.99999981196Now - Start = 5000 driftNanos = 328105.9999999343Now - Start = 6000 driftNanos = 393130.99999981205Now - Start = 7000 driftNanos = 458913.9999998224Now - Start = 8000 driftNanos = 524811.9999996561Now - Start = 9000 driftNanos = 590093.9999992261Now - Start = 10000 driftNanos = 656146.9999996916   //间隔很平滑Now - Start = 11000 driftNanos = 721020.0000008626Now - Start = 12000 driftNanos = 786994.0000000497  

注意看最大值的增长,在Solaris上很稳定,而在Linux上相当一段时间内看起来都OK,然后出现了大的跳跃。我们在选择示例代码时相当认真,尽量避免创建额外的线程,甚至对象,以将平台的干预降到最低(比如说,没有对象的创建就意味着不会做垃圾收集),但即便如此,我们还是能看到JVM的影响。

最终证实Linux时序上出现的跳跃是由不同CPU上的TSC计数器之间的差异造成的。JVM会定期挂起正在运行的Java线程,并将它迁移到不同核心上。所以程序代码会见到不同CPU计数器上的差异。

这就是说对于间隔较长的时间,nanoTime基本上是不可信的。只能用它测量较短的时间间隔,较长(宏观)的时间间隔应该用currentTimeMillis重新校准。

要充分掌握性能调优,即要有扎实的测量理论,还需要知道实现细节。

6.4.3 时间在性能调优中的作用

要做好性能调优,你必须知道该如何解读代码运行期间得到的测量记录,也就是说你必须明白在Java平台上得到的时间测量结果的局限性。