System.nanoTime() 完全无用吗?

160
根据博客文章“谨防Java中的System.nanoTime()方法”,在x86系统中,Java的System.nanoTime()方法使用特定于CPU的计数器返回时间值。现在考虑我用来测量函数调用时间的以下情况:
long time1= System.nanoTime();
foo();
long time2 = System.nanoTime();
long timeSpent = time2-time1;

在多核系统中,测量time1后,线程可能被调度到另一个处理器,该处理器的计数器小于之前的处理器。因此,我们可能会得到一个time2的值,该值比time1 。因此,在timeSpent中,我们将得到一个负值。

考虑到这种情况,System.nanotime现在几乎没有用处了吗?

我知道更改系统时间不会影响nanotime。这不是我上面描述的问题。问题在于每个CPU自开机以来都会保留一个不同的计数器。第二个CPU上的计数器可以比第一个CPU上的计数器低。由于线程可能在获取time1后由操作系统调度到第二个CPU,因此timeSpent的值可能不正确,甚至为负。


2
我没有答案,但我同意你的观点。也许这应该被视为JVM中的一个bug。 - Aaron Digulla
2
那篇文章是错误的,不使用TSC会很慢,但你必须接受现实:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6440250 此外,通过虚拟机,TSC可以变得有用,但这样又会变慢。 - bestsss
1
当然,你可以在虚拟机中运行,在会话过程中CPU可能会出现:D - lmat - Reinstate Monica
14个回答

213

这篇答案是在2011年从Sun JDK的角度出发,介绍当时操作系统上运行的情况。那已经是很久以前的事情了!leventov's 答案提供了更为实时的视角。

那篇帖子是错误的,nanoTime是安全的。该帖子中有一条评论链接到David Holmes的博客文章,他是Sun公司的实时和并发专家。该文章说:

System.nanoTime()是使用QueryPerformanceCounter/QueryPerformanceFrequency API实现的。[...] QPC使用的默认机制由硬件抽象层(HAL)确定。[...] 这个默认值不仅在硬件之间而且还在操作系统版本之间改变。例如,Windows XP Service Pack 2由于TSC在SMP系统中没有被同步在不同处理器上,并且其频率可以基于电源管理设置而变化(因此与经过的时间的关系也会发生变化),因此更改了默认值,使用电源管理计时器(PMTimer)。

因此,在Windows上,这个问题直到WinXP SP2之前都是一个问题,但现在不是了。

我找不到其他平台的第二部分(或更多内容),但是该文章确实包含一条备注,Linux已经遇到并以同样的方式解决了相同的问题,并链接到了clock_gettime(CLOCK_REALTIME)的 FAQ,其中写道:

  1. clock_gettime(CLOCK_REALTIME)是否在所有处理器/核心上保持一致?(是否与体系结构有关?例如ppc、arm、x86、amd64、sparc等)。

它应该保持一致,否则会被视为有缺陷。

然而,在x86 / x86_64上,可能会出现未同步或变频TSC导致时间不一致的情况。 2.4内核在这方面没有保护措施,早期的2.6内核也做得不太好。从2.6.18开始,检测此问题的逻辑更好,并且通常会回退到安全的时钟源。

ppc始终具有同步的时间基准,因此不应该是一个问题。

因此,如果Holmes的链接可以被解释为意味着nanoTime调用clock_gettime(CLOCK_REALTIME),则在x86上的内核2.6.18及更高版本以及PowerPC上始终安全(因为IBM和Motorola知道如何设计微处理器,而Intel不知道)。

遗憾的是,没有提到SPARC或Solaris。当然,我们不知道IBM JVM会怎么做。但是,现代Windows和Linux上的Sun JVM可以正确处理这个问题。

编辑:此答案基于其引用的来源。但我仍然担心它可能完全错误。一些更加最新的信息将非常有价值。我刚刚遇到了一个关于Linux时钟的四年新的文章,可能会有用。


14
即使是WinXP SP2似乎也受到影响。在单个Athlon 64 X2 4200+处理器上运行原始代码示例,使用void foo() { Thread.sleep(40); },我得到了一个负时间(-380毫秒!)。 - Luke Usherwood
我不确定在Linux、BSD或其他平台上有关于这方面的更新吗? - Tomer Gabel
6
好的回答,应该添加一个链接到这个话题更近期的探讨:http://shipilev.net/blog/2014/nanotrusting-nanotime/ - Nitsan Wakart
1
@SOFe:哦,那真是太遗憾了。幸运的是,它在网络档案馆中有备份。我会尝试找到当前版本。 - Tom Anderson
2
注意:在OpenJDK 8u192之前,OpenJDK没有遵守规范,请参见https://bugs.openjdk.java.net/browse/JDK-8184271。请确保使用至少最新版本的OpenJDK 8或OpenJDK 11+。 - leventov
显示剩余2条评论

37
自Java 7起,JDK规范保证System.nanoTime()是安全的。 System.nanoTime()的Javadoc清楚地表明,观察到的所有JVM内部调用都是单调的(即跨所有线程):

返回值代表自某个固定但任意的原始时间以来的纳秒数(可能在未来,因此值可能为负)。 在Java虚拟机实例中的所有对此方法的调用使用相同的起点; 其他虚拟机实例可能会使用不同的起源。

JVM/JDK实现负责消除在调用底层OS实用程序时可能观察到的不一致性(例如,在 Tom Anderson的答案 中提到的那些不一致性)。
大多数其他旧答案(写于2009年至2012年)表达了恐惧、不确定和怀疑,这些问题可能与Java 5或Java 6有关,但对于现代版本的Java已经不再相关。值得一提的是,尽管JDK保证了nanoTime()的安全性,但在某些平台或特定情况下,OpenJDK中存在几个bug导致其无法保持此保证(例如:JDK-8040140JDK-8184271)。目前,在OpenJDK中,没有关于nanoTime()的已知的未修复的bug,但是新发现这样的bug或者在新版本的OpenJDK中出现回归问题应该不会让任何人感到惊讶。
因此,对于使用nanoTime()进行定时阻塞、间隔等待、超时等操作的代码,最好将负时间差(超时)视为零而不是抛出异常。这种做法也更加优越,因为它符合java.util.concurrent.*中所有类的所有定时等待方法的行为,例如Semaphore.tryAcquire()Lock.tryLock()BlockingQueue.poll()等。

然而,在实现定时阻塞、间隔等待、超时等功能时,仍应优先使用 nanoTime() 而不是 currentTimeMillis(),因为后者容易受到“时间倒流”现象的影响(例如由于服务器时间校正等原因),即currentTimeMillis()根本不适合用于测量时间间隔。有关更多信息,请参见此答案

不要直接使用 nanoTime() 进行代码执行时间测量,最好使用专门的基准测试框架和分析器,例如JMHasync-profiler壁钟分析模式下。


35

经过一番搜索,我发现如果一个人非常严谨的话,那么在某些特定情况下可能会被认为是无用的......这取决于您的要求有多时间敏感...

可以查看来自Java Sun网站的此引用

实时时钟和System.nanoTime()都基于相同的系统调用和相同的时钟。

使用Java RTS,所有基于时间的API(例如计时器、周期线程、截止日期监视等)都基于高分辨率计时器。并且,与实时优先级一起,它们可以确保适当的代码将按照实时约束在正确的时间执行。相比之下,普通的Java SE API只提供了一些能够处理高分辨率时间的方法,不能保证在给定时间执行。在代码中使用System.nanoTime()进行已经过时间的测量应该始终是准确的。

Java还有一个关于nanoTime()方法的警告

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

看起来唯一的结论是不能依靠nanoTime()作为精确值。因此,如果您不需要测量仅相差纳秒的时间,则此方法足够好,即使返回值为负数也可以。然而,如果您需要更高的精度,他们似乎建议您使用JAVA RTS。

因此回答您的问题...不,nanoTime()并不无用...只是在每种情况下使用它不是最明智的方法。


3
即使返回值为负数,这种方法仍然足够好。我不明白,如果timespent的值为负数,那么它如何在衡量foo()所用时间方面有用呢? - pdeva
3
没问题,因为你所担心的只是差异的绝对值。例如,如果你的测量结果是时间t,其中t=t2-t1,那么你想知道|t|,即使这个值是负数也无妨。即使存在多核问题,影响通常也只会是几个纳秒而已。 - mezoid
3
备份@ Aaron:t2和t1都可能是负数,但(t2-t1)不应为负数。 - jfs
2
aaron:这正是我的观点。t2-t1永远不应该为负数,否则我们就有了一个bug。 - pdeva
12
@pdeva - 但是你误解了文档的意思。你在提出一个无关紧要的问题。 存在一个被认为是“0”的时间点。nanoTime()返回的值是相对于该时间点准确的。它是一个单调递增的时间线。你可能只是从该时间线的负部分获取了一系列数字:“-100”,“-99”,“-98”(实际上要大得多)。它们沿着正确的方向增加,所以这里没有任何问题。 - ToolmakerSteve
显示剩余4条评论

18

不需要争论,只需使用源代码。在这里,SE 6适用于Linux,您可以自行得出结论:

jlong os::javaTimeMillis() {
  timeval time;
  int status = gettimeofday(&time, NULL);
  assert(status != -1, "linux error");
  return jlong(time.tv_sec) * 1000  +  jlong(time.tv_usec / 1000);
}


jlong os::javaTimeNanos() {
  if (Linux::supports_monotonic_clock()) {
    struct timespec tp;
    int status = Linux::clock_gettime(CLOCK_MONOTONIC, &tp);
    assert(status == 0, "gettime error");
    jlong result = jlong(tp.tv_sec) * (1000 * 1000 * 1000) + jlong(tp.tv_nsec);
    return result;
  } else {
    timeval time;
    int status = gettimeofday(&time, NULL);
    assert(status != -1, "linux error");
    jlong usecs = jlong(time.tv_sec) * (1000 * 1000) + jlong(time.tv_usec);
    return 1000 * usecs;
  }
}

7
只有当你知道所使用的API的作用时,这才有帮助。所使用的API是由操作系统实现的;这段代码根据所使用API的规范(clock_gettime/gettimeofday)编写是正确的,但正如其他人指出的那样,一些不最新的操作系统会有错误的实现。 - Blaisorblade

7

Linux可以矫正不同CPU之间的差异,但Windows不能。建议您假设System.nanoTime()只有约1微秒的精度。获得更长时间的简单方法是调用foo() 1000次或更多次,并将时间除以1000。


2
你能提供一个参考吗?(在Linux和Windows上的行为)? - jfs
1
不幸的是,所提出的方法通常会非常不精确,因为每个事件都会落入+/- 100ms的墙钟更新时间段,往往会对亚秒操作返回零。9个持续时间为零的操作的总和是零,除以九等于...零。相反,使用System.nanoTime()将提供相对准确(非零)的事件持续时间,然后将其求和并除以事件数量将提供高度精确的平均值。 - Darrell Teague
@DarrellTeague 对于1000个事件进行求和并将它们相加,与端到端时间是相同的。 - Peter Lawrey
@DarrellTeague System.nanoTime()在大多数系统上精度达到1微秒或更好(而不是100,000微秒)。仅当您的操作只有几微秒,并且仅适用于某些系统时,才平均多个操作才相关。 - Peter Lawrey
1
很抱歉,关于“求和”事件中使用的语言存在一些混淆。是的,如果在进行1000个子秒操作之前标记时间,然后在结束时再次标记时间并进行除法运算-这对于某些正在开发中的系统来说可以得到给定事件持续时间的良好近似值。 - Darrell Teague
根据System.nanoTime()规范,这是不正确的。JVM负责解决所有操作系统上的差异。自8u192以来,OpenJDK似乎已经成功解决了这个问题,其中https://bugs.openjdk.java.net/browse/JDK-8184271已经被修复。 - leventov

5

绝不是无用的。时间迷们正确地指出了多核问题,但在实际应用中,它通常比currentTimeMillis()更好。

当计算帧刷新中图形位置时,nanoTime()在我的程序中导致运动更加平滑。

而且我只在多核机器上测试。


5

我曾经看到使用System.nanoTime()报告了负的已过时间。为了明确,涉及到的代码如下:

    long startNanos = System.nanoTime();

    Object returnValue = joinPoint.proceed();

    long elapsedNanos = System.nanoTime() - startNanos;

变量'elapsedNanos'的值为负数。(我确定中间调用的时间不到293年,这是long类型存储纳秒的溢出点 :)

这个问题出现在IBM v1.5 JRE 64位版本上,运行在AIX系统的IBM P690(多核)硬件上。我只见过一次这种错误,所以它似乎非常罕见。我不知道原因——是硬件特定问题,JVM缺陷——我不知道。我也不知道nanoTime()的准确性有什么影响。

回答最初的问题,我认为nanoTime并不无用——它提供了亚毫秒级别的计时,但实际上(而不仅仅是理论上)存在不准确的风险,需要考虑这一点。


唉,似乎是一些操作系统/硬件问题。文档说明核心值可能为负数,但(较大的负数减去较小的负数)仍应为正值。确实假设在同一线程中,nanoTime()调用应始终返回正值或负值。多年来,在各种Unix和Windows系统上从未见过这种情况,但如果硬件/操作系统将这个看似原子的操作分配到不同的处理器上,这种情况听起来是有可能的。 - Darrell Teague
@BasilVandegriend 这不是一个 bug。根据文档,你示例中的第二个 System.nanoTime() 很少会在另一个 CPU 上运行,并且在该 CPU 上计算得到的 nanoTime 值可能比在第一个 CPU 上计算得到的值低。因此,经过时间计算后得到一个负值是有可能的。 - endless

2

我在此提供一个链接,这里的讨论与Peter Lawrey给出的好答案实际上是相同的。 为什么使用System.nanoTime()会得到负的经过时间?

许多人提到,在Java中,System.nanoTime()可能会返回负时间。很抱歉重复了别人说过的话。

  1. nanoTime()不是时钟,而是CPU周期计数器。
  2. 返回值除以频率后看起来像是时间。
  3. CPU频率可能会波动。
  4. 当您的线程在另一个CPU上调度时,有可能获得nanoTime(),结果是负差值。这是有道理的。跨CPU的计数器没有同步。
  5. 在许多情况下,你可能会得到相当误导性的结果,但你无法告诉因为delta不是负的。想一想。
  6. (未经证实)我认为即使在同一个CPU上,如果指令被重新排序,您也可能会得到负面的结果。为了防止这种情况,您需要调用一个内存屏障,序列化您的指令。

如果System.nanoTime()返回执行它的coreID,那就太酷了。


1
除了3和5之外,其他都是错误的。1. nanoTime()不是CPU周期计数器,而是纳秒时间。2.产生nanoTime值的方式是平台特定的。4.根据nanoTime()规范,差异不能为负。假设OpenJDK没有与nanoTime()相关的错误,并且目前没有已知的未解决错误。 6.nanoTime调用不能在线程内重新排序,因为它是本地方法,JVM尊重程序顺序。 JVM从不重新排序本地方法调用,因为它不知道它们内部发生了什么,因此无法证明这样的重新排序是安全的。 - leventov
@leventov,nanotime()函数是安全的吗?也就是说,它不会返回负值,并且在时间方面准确无误。我不明白为什么要公开一个存在问题的API函数。这篇文章是证明,从2009年开始到2019年仍然有人评论。对于关键任务,我想人们会依赖像Symmetricom这样的定时卡。 - Vortex
@leventon - 你对第四点是错误的。几年前,在多线程JVM中,nanoTime()确实会返回负值。这是事实。 - Vortex
是的,nanoTime是安全的(自2011年JDK 7发布以来,除了OpenJDK中的错误之外,其中最后一个(如上所述)已在2017年修复)。 - leventov
1
你的第四点提到了负差值,而不是负值:“有可能得到nanoTime()的结果是负差值。” - leventov
显示剩余2条评论

2
不,不是这样的......它取决于你的 CPU,查看 高精度事件计时器 以了解根据 CPU 处理事物时的不同处理方式如何/为什么。

基本上,阅读你的 Java 源代码,并检查你的版本对该函数的操作以及它是否适用于你将要运行它的 CPU。

IBM 甚至建议 使用它进行性能基准测试(一个2008年的帖子,但已更新)。

作为所有实现定义行为,"买家需谨慎!" - David Schmitt

2

在运行Windows XP和JRE 1.5.0_06的Core 2 Duo上,这似乎不是一个问题。

在使用三个线程进行测试时,我没有看到System.nanoTime()倒退。处理器都很忙碌,线程偶尔休眠以促使线程移动。

[编辑] 我猜这只会发生在物理上分离的处理器上,即多个内核的计数器在同一芯片上同步。


2
这种情况可能不会经常发生,但由于nanotime()的实现方式,这种可能性始终存在。 - pdeva
我猜这只会发生在物理上分离的处理器上,也就是说,对于同一芯片上的多个核心,计数器是同步的。 - starblue
即使如此,这也取决于具体的实现,如果我没记错的话。但这是操作系统应该处理的事情。 - Blaisorblade
1
同一x86处理器上的多个核心上的RDTSC计数器不一定同步 - 一些现代系统允许不同的核心以不同的速度运行。 - Jules

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接