【问题标题】:Why do System.nanoTime() and System.currentTimeMillis() drift apart so rapidly?为什么 System.nanoTime() 和 System.currentTimeMillis() 如此迅速地分开?
【发布时间】:2011-08-15 21:34:43
【问题描述】:

出于诊断目的,我希望能够在长时间运行的服务器应用程序中检测系统时钟的变化。由于System.currentTimeMillis() 基于挂钟时间,System.nanoTime() 基于独立于挂钟时间(*)的系统计时器,我想我可以使用这些值之间差异的变化来检测系统时间变化。

我编写了一个快速测试应用程序,以查看这些值之间的差异有多稳定,令我惊讶的是,这些值立即以每秒几毫秒的水平出现差异。有几次我看到了更快的分歧。这是在带有 Java 6 的 Win7 64 位桌面上。我还没有在 Linux(或 Solaris 或 MacOS)下尝试过这个测试程序来查看它的性能。对于此应用程序的某些运行,差异是正的,对于某些运行,它是负的。这似乎取决于桌面在做什么,但很难说。

public class TimeTest {
  private static final int ONE_MILLION  = 1000000;
  private static final int HALF_MILLION =  499999;

  public static void main(String[] args) {
    long start = System.nanoTime();
    long base = System.currentTimeMillis() - (start / ONE_MILLION);

    while (true) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        // Don't care if we're interrupted
      }
      long now = System.nanoTime();
      long drift = System.currentTimeMillis() - (now / ONE_MILLION) - base;
      long interval = (now - start + HALF_MILLION) / ONE_MILLION;
      System.out.println("Clock drift " + drift + " ms after " + interval
                         + " ms = " + (drift * 1000 / interval) + " ms/s");
    }
  }
}

Thread.sleep() 时间的不准确以及中断应该与计时器漂移完全无关。

这两个 Java“系统”调用都旨在用作测量 - 一个用于测量挂钟时间的差异,另一个用于测量绝对间隔,因此当实时时钟未更改时,这些值应该以非常接近相同的速度变化,对吧?这是 Java 中的错误、弱点还是失败?操作系统或硬件中是否存在阻止 Java 更准确的东西?

我完全预计这些独立测量之间会有一些漂移和抖动 (**),但我预计每天的漂移不到一分钟。每秒 1 毫秒的漂移,如果单调,几乎是 90 秒!我观察到的最坏情况下的漂移可能是那个的十倍。每次我运行这个程序时,我都会在第一次测量时看到漂移。到目前为止,我运行程序的时间没有超过 30 分钟。

由于抖动,我希望在打印的值中看到一些小的随机性,但在程序的几乎所有运行中,我看到差异稳步增加,通常每秒增加 3 毫秒,增加几倍不止于此。

是否有任何版本的 Windows 具有类似于 Linux 的机制来调整系统时钟速度以缓慢地使时钟时钟与外部时钟源同步?这样的事情会影响两个计时器,还是只影响挂钟计时器?

(*) 我知道在某些架构上,System.nanoTime() 必然会使用与System.currentTimeMillis() 相同的机制。我还认为可以公平地假设任何现代 Windows 服务器都不是这样的硬件架构。这是一个糟糕的假设吗?

(**) 当然,System.currentTimeMillis() 的抖动通常比System.nanoTime() 大得多,因为它在大多数系统上的粒度不是 1 毫秒。

【问题讨论】:

  • GetSystemTimeAdjustment() 函数将返回有关调整是否处于活动状态及其参数设置的信息。
  • 哈!你觉得你有问题?!我在一小时内看到了多达 0.01 的漂移,即在 System.nanoTime() 增加了 3600 000 000 000 之后,只过去了 59 分半钟!

标签: java windows datetime time


【解决方案1】:

您可能会对this Sun/Oracle blog post about JVM timers 感兴趣。

以下是那篇文章中关于 Windows 下的 JVM 计时器的几段:

System.currentTimeMillis() 是使用GetSystemTimeAsFileTime 方法实现的,该方法本质上只是读取 Windows 维护的低分辨率时间值。读取这个全局变量自然非常快——根据报告的信息,大约需要 6 个周期。无论定时器中断是如何编程的,这个时间值都会以恒定速率更新 - 取决于平台,这将是 10 毫秒或 15 毫秒(这个值似乎与默认中断周期相关)。

System.nanoTime() 使用QueryPerformanceCounter / QueryPerformanceFrequency API 实现(如果可用,则返回currentTimeMillis*10^6)。 QueryPerformanceCounter(QPC) 根据运行的硬件以不同的方式实现。通常,它将使用可编程间隔定时器 (PIT)、ACPI 电源管理定时器 (PMT) 或 CPU 级时间戳计数器 (TSC)。访问 PIT/PMT 需要执行慢速 I/O 端口指令,因此 QPC 的执行时间为微秒级。相比之下,读取 TSC 大约需要 100 个时钟周期(从芯片中读取 TSC 并将其转换为基于工作频率的时间值)。您可以通过检查 QueryPerformanceFrequency 是否返回签名值 3,579,545(即 3.57MHz)来判断您的系统是否使用 ACPI PMT。如果您看到 1.19Mhz 左右的值,那么您的系统使用的是旧的 8245 PIT 芯片。否则,您应该会看到一个近似于您的 CPU 频率的值(以任何可能生效的速度限制或电源管理为模。)

【讨论】:

  • 我已经找到了相同的博客条目。这并不能完全回答我的问题,但我认为这与我将得到的答案一样接近。
  • Sun 有 documented a bug 关于损坏的 System.nanoTime() 直到 2015 年才被标记为修复。它是针对 Linux 提起的,但在 cmets 中指出它也影响了 Windows 系统。同样,这个答案的文本取自 2006 年的一篇博客文章,在随后几年的几份关键开发报告中基本上是 refuted。有意义的部分是声明,“根据运行的硬件以不同的方式实现。”
  • 在错误报告中声称它只影响 Java 5 和 6。我必须看到在以后的版本中应用的修复的进一步证明才能满足解决方案。 (它只是说它影响 Java 5/6 是因为原始报告的日期吗?)无论如何,这似乎非常依赖于实现,并且受制于系统之间的不一致。
【解决方案2】:

"返回最精确的可用系统计时器的当前值,以纳秒为单位。

“此方法只能用于测量经过的时间,与任何其他系统或挂钟时间概念无关。返回的值表示自某个固定但任意时间以来的纳秒(可能在将来,因此值可能为负)。此方法提供纳秒精度,但不一定提供纳秒精度。不保证值更改的频率。跨越大约 292 年(2**63 纳秒)的连续调用的差异将无法准确计算经过的时间由于数值溢出。”

请注意,它说的是“精确”,而不是“准确”。

这不是“Java 中的错误”或任何东西中的“错误”。这是一个定义。 JVM 开发人员四处寻找并使用系统中最快的时钟/计时器。如果这与系统时钟同步,那么很好,但如果不是,那就是 cookie 崩溃的方式。例如,计算机系统将有一个准确的系统时钟,但内部有一个与 CPU 时钟速率或其他类似的更高速率的计时器,这是完全合理的。由于时钟频率经常变化以最大限度地降低功耗,因此该内部定时器的递增率会有所不同。

【讨论】:

  • JavaDoc 说“返回的值表示自某个固定但任意时间以来的纳秒”,这告诉我该值的增量率不应随时间变化(很大)。如果你的 CPU 时钟频率下降了一半,而这个计数器的增量率下降了一半,那么你就不能很好地用它来测量经过的时间,对吗?你描述它的方式,除了时间是否流逝之外,它并不能衡量一个人可以依赖的任何东西。
【解决方案3】:

System.currentTimeMillis() 和 System.nanoTime() 不一定由 相同的硬件。 System.currentTimeMillis(),由 GetSystemTimeAsFileTime() 支持 具有 100ns 分辨率元素。它的来源是系统定时器。 System.nanoTime() 由系统的高性能计数器提供支持。有各种各样不同的硬件 提供这个计数器。因此,它的分辨率会有所不同,具体取决于底层硬件。

在任何情况下都不能假设这两个源是同相的。测量两个值 互相对抗会透露出不同的跑动速度。如果把System.currentTimeMillis()的更新作为时间上的真实进度,System.nanoTime()的输出可能时而慢,时而快,而且变化不定。

为了锁相这两个时间源,必须进行仔细校准。

这两个时间源之间的关系更详细的描述可以找到 在Windows Timestamp Project。

【讨论】:

    【解决方案4】:

    我不确定这实际上有多大帮助。但这是 Windows/Intel/AMD/Java 世界中一个正在发生积极变化的领域。几年(至少 10 年)以来,对准确和精确的时间测量的需求已经很明显了。英特尔和 AMD 都通过改变 TSC 的工作方式做出了回应。两家公司现在都有称为 Invariant-TSC 和/或 Constant-TSC 的东西。

    查看rdtsc accuracy across CPU cores。引用自 osgx(参考 Intel 手册)。

    "16.11.1 不变 TSC

    较新处理器中的时间戳计数器可能支持增强功能,称为不变 TSC。处理器对不变 TSC 的支持由 PUID.80000007H:EDX[8] 表示。

    不变的 TSC 将在所有 ACPI P-、C- 中以恒定速率运行。和 T 状态。这是向前发展的架构行为。在具有不变 TSC 支持的处理器上,操作系统可以将 TSC 用于挂钟计时器服务(而不是 ACPI 或 HPET 计时器)。 TSC 读取效率更高,并且不会产生与环转换或访问平台资源相关的开销。”

    另见http://www.citihub.com/requesting-timestamp-in-applications/。引用作者的话

    对于 AMD:

    如果 CPUID 8000_0007.edx[8] = 1,则确保 TSC 速率在所有 P-States、C-States 和 stop-grant 转换(例如 STPCLK Throttling)中保持不变;因此,TSC 适合用作时间来源。

    对于英特尔:

    处理器对不变 TSC 的支持由 CPUID.80000007H:EDX[8] 指示。不变的 TSC 将在所有 ACPI P-、C- 中以恒定速率运行。和 T 状态。这是向前发展的架构行为。在具有不变 TSC 支持的处理器上,操作系统可以将 TSC 用于挂钟计时器服务(而不是 ACPI 或 HPET 计时器)。 TSC 读取效率更高,并且不会产生与环转换或访问平台资源相关的开销。”

    现在真正重要的一点是,最新的 JVM 似乎利用了新近可靠的 TSC 机制。网上没有太多可以展示这一点。不过,请查看http://code.google.com/p/disruptor/wiki/PerformanceResults。

    “为了测量延迟,我们采用三阶段管道并在低于饱和度的情况下生成事件。这是通过在注入一个事件后等待 1 微秒后再注入下一个事件并重复 5000 万次来实现的。要以这种精度水平计时必须使用来自 CPU 的时间戳计数器。我们选择具有不变 TSC 的 CPU,因为较旧的处理器会因省电和睡眠状态而改变频率。Intel Nehalem 和以后的处理器使用可以被最新 Oracle 访问的不变 TSC在 Ubuntu 11.04 上运行的 JVM。此测试未使用 CPU 绑定"

    请注意,“Disruptor”的作者与在 Azul 和其他 JVM 上工作的人有着密切的联系。

    另请参阅“幕后的 Java 飞行记录”。本演示文稿提到了新的不变 TSC 指令。

    【讨论】:

      【解决方案5】:

      是否有任何版本的 Windows 具有类似于 Linux 的机制来调整系统时钟速度以缓慢地使时钟时钟与外部时钟源同步?这样的事情会影响两个计时器,还是只影响挂钟计时器?

      Windows Timestamp Project 可以满足您的要求。据我所知,它只会影响挂钟计时器。

      【讨论】:

        猜你喜欢
        • 2013-10-03
        • 1970-01-01
        • 1970-01-01
        • 2023-03-06
        • 2012-05-17
        • 2020-12-20
        • 2016-11-28
        • 1970-01-01
        相关资源
        最近更新 更多