System.currentTimeMillis与System.nanoTime的区别

415

准确性 VS 精度

我想知道在更新游戏中对象的位置时,是否应该使用 System.currentTimeMillis() 还是 System.nanoTime()?它们的运动变化与自上次调用以来经过的时间成正比,而我希望尽可能精确。

我读到不同操作系统之间存在一些严重的时间分辨率问题(即 Mac / Linux 几乎具有 1 毫秒的分辨率,而 Windows 则具有 50 毫秒的分辨率?)。我主要在 Windows 上运行我的应用程序,50 毫秒的分辨率似乎相当不准确。

除了我列出的两个选项外,还有更好的选择吗?

有什么建议/评论吗?


92
nanoTime通常比currentTimeMillis更准确,但是也是一个相对昂贵的调用。currentTimeMillis()运行在几个(5-6)CPU时钟内,而nanoTime取决于底层架构,可能需要100多个CPU时钟。 - bestsss
17
你们都知道Windows通常的时间片粒度是1000毫秒/64,对吧?也就是15.625毫秒或15625000纳秒! - user968961
8
我认为多用一百个时钟周期不会影响你的游戏,而且这种牺牲可能是值得的。你应该每次游戏更新只调用一次该方法,然后将值保存在内存中,这样不会增加很多开销。至于不同平台的细粒度,我不清楚。 - aglassman
14
Windows 的默认时间片粒度为1000毫秒/64。你可以通过本地的timeBeginPeriod API来增加它。现代PC除了基本计时器外还有高分辨率计时器。高分辨率计时器可通过QueryPerformanceCounter调用访问。 - Robin Davies
@bestsss 如何查找CPU时钟使用情况? - Gohan
3
@Gohan - 这篇文章详细介绍了 System.currentTimeMillis() 方法的内部运作机制:http://pzemtsov.github.io/2017/07/23/the-slow-currenttimemillis.html - Attila Tanyi
10个回答

345
如果您只是想要极其精确的经过时间测量值,请使用System.nanoTime()System.currentTimeMillis()将给出自纪元以来以毫秒为单位的最准确的经过时间,但System.nanoTime()会相对于某个任意点给出纳秒精确时间。
public static long nanoTime()

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

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

例如,要测量代码执行的时间:

long startTime = System.nanoTime();    
// ... the code being measured ...    
long estimatedTime = System.nanoTime() - startTime;

更多信息请参见:JavaDoc System.nanoTime()JavaDoc System.currentTimeMillis()


22
你确定你知道准确度和精确度之间的区别吗?以纳秒的精度来说,它不可能是准确的。 - mmcdole
7
抱歉,我原意是要表达“精确”,但我使用了这个词的不严谨含义,我同意这会导致混淆(并且是错误的用法)。 - dancavallaro
4
@dancavallaro,感谢提供信息。如果您不介意的话,我会编辑您的回答并引用文档中的一段内容,并修复链接。 - mmcdole
153
这个答案在选择nanoTime()方面技术上是正确的,但完全忽略了一个非常重要的点。如文档所述,nanoTime()是一个精度计时器。currentTimeMillis()不是计时器,它是“挂钟时间”。nanoTime()始终会产生正的经过时间,而currentTimeMillis不会(例如如果你更改了日期、遇到闰秒等)。对于某些类型的系统来说,这是一个非常重要的区别。 - charstar
13
用户更改时间和NTP同步可以导致currentTimeMillis变化,但为什么DST会导致它的变化呢? DST切换并不会改变自纪元以来经过的秒数。它可能是一个“挂钟”,但基于UTC。您必须根据所在时区和DST设置确定其对应本地时间的时间(或使用其他Java工具来为您完成)。 - Shadow Man
显示剩余4条评论

123

由于没有人提到这一点...

在不同的JVM之间比较System.nanoTime()调用的结果是不安全的,每个JVM可能有独立的“起源”时间。

System.currentTimeMillis()会返回(大约)相同的值,因为它与系统壁钟时间相关联。

如果您想计算两个事件之间经过的时间,例如秒表,请使用nanoTime();系统壁钟的更改使得currentTimeMillis()在此 use case 中不正确。


4
据此网址(https://dev59.com/anRB5IYBdhLWcg3wyqKo),在Windows操作系统中仅适用于SP2之前的版本。 - Peter Schmitz
4
很有趣... 有没有参考资料支持 "不安全地比较不同线程之间的System.nanoTime() 调用结果"? 以下链接值得一看: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6519418 http://docs.oracle.com/javase/7/docs/api/java/lang/System.html#nanoTime() - user454322
19
根据这里提到的描述:http://docs.oracle.com/javase/7/docs/api/java/lang/System.html#nanoTime(),看起来nanoTime返回的值只有在同一个JVM实例内计算两个值之间的差异时才有意义,可以进行比较。 - Tuxdude
3
nanoTime()方法在多线程环境下返回值是一致的,除非你的系统出现问题。调用该方法返回的数字会按预期递增。相比之下,System.currentTimeMillis()方法可能会在同一个线程内跳跃后退,因为它表示的是墙上时间,可以受到外部影响因素(例如NTP更新)的影响而进行调整。 - David
9
nanoTime() 方法的 JavaDoc(文档注释)中写道:“所有调用此方法的实例在同一 Java 虚拟机下使用相同的起始时间;其他虚拟机实例可能会使用不同的起始时间。” 这意味着它将在不同线程间返回相同的值。 - Simon Forsberg
显示剩余4条评论

60

更新自 Arkadiy:我在 Oracle Java 8 中观察到了更准确的System.currentTimeMillis()行为。时间以1毫秒的精度返回。 OpenJDK中的源代码没有变化,所以我不知道是什么导致了更好的行为。


Sun 的 David Holmes 几年前发布了一篇博客文章,非常详细地介绍了 Java 时间 API(特别是System.currentTimeMillis()System.nanoTime()),您应该在什么情况下使用它们以及它们如何在内部工作。

Hotspot VM 内部:时钟、定时器和调度事件 - Part I - Windows

Java 在 Windows 上用于具有定时等待参数的 API 的计时器的一个非常有趣的方面是计时器的分辨率可能会因为其他 API 调用而改变 - 不仅在特定进程中,而是在整个系统范围内。他展示了一个例子,在这个例子中使用 Thread.sleep() 会导致分辨率的变化。


1
@Arkadiy:你有更新中所述的声明的来源吗? - Lii
@Lii - 不幸的是,不行。它来自我在这个问题中运行代码:http://stackoverflow.com/questions/34090914/currenttimemillis-and-waiting-by-spinning-on-nanotime。该代码使用Java 7可以产生15毫秒精度,使用Java 8可以产生1毫秒精度。 - user3458
2
我将currentTimeMillis追踪到了OpenJDK中的hotspot/src/os/windows/vm/os_windows.cpp的os::javaTimeMillis(http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/os/windows/vm/os_windows.cpp#l833)。看起来仍然是GetSystemTimeAsFileTime,所以我不知道更改来自哪里。或者是否有效。在使用之前进行测试。 - user3458
行为变化是由于XP和7中GetSystemTimeAsFileTime的工作方式不同导致的。请参见此处获取更多详细信息(简而言之,自整个系统引入了一些更精确的计时方法以来,它变得更加精确)。 - user719662

12

正如其他人所说,currentTimeMillis是时钟时间,由于夏令时(注意:夏令时与时区与currentTimeMillis无关,其余内容属实)的改变、用户更改时间设置、闰秒和互联网时间同步而发生变化。如果您的应用程序依赖于单调递增的经过时间值,您可能更喜欢使用nanoTime。

您可能认为在游戏中玩家不会搞乱时间设置,也许您是对的。但不要低估因互联网时间同步或远程桌面用户而引起的干扰。nanoTime API免疫此类干扰。

如果您想使用时钟时间,但避免由于互联网时间同步而导致不连续性,您可以考虑使用像Meinberg这样的NTP客户端,该客户端“调整”时钟速率以将其归零,而不仅仅是定期重置时钟。

我从个人经验中说起。在我开发的一款天气应用程序中,我偶尔会遇到风速突然升高的情况。我花了一些时间才意识到我的时间基准受到了典型PC上的时钟时间行为的干扰。当我开始使用nanoTime时,所有问题都消失了。对我的应用程序而言,一致性(单调性)比原始精度或绝对精度更为重要。


31
currentTimeMillis是时钟时间,由于夏令时的存在而改变,这个说法很可能是错误的。System.currentTimeMillis()报告自 Unix/Posix纪元(1970年1月1日协调世界时午夜)以来经过的时间(以毫秒为单位)。由于协调世界时从不调整夏令时,因此当本地时区为夏令时添加或减去偏移量时,该值将不会受到影响。此外,Java时间刻度平滑跳秒,使得每天仍然显示为86400秒。 - scottb

11

System.nanoTime()在较旧的JVM上不受支持。如果这是一个问题,建议使用currentTimeMillis

关于准确性,你几乎是正确的。在某些Windows机器上,currentTimeMillis()的分辨率大约为10毫秒(而不是50毫秒)。我不确定为什么,但有些Windows机器与Linux机器一样准确。

我过去曾经使用GAGETimer并取得了适度的成功。


4
“older JVMs”指的是哪些版本?是Java 1.2或者其他什么版本? - Simon Forsberg
3
System.nanoTime()是在2004年的Java 1.5中引入的。Java 1.4于2013年延长了支持,因此可以很放心地说,现在可以依赖System.nanoTime(),这个答案现在已经过时了。 - Dave L.

5

如果需要这样的精确度,请使用System.nanoTime(),但请注意,这将需要Java 5+ JVM。

在我的XP系统上,使用以下代码报告至少278纳秒的系统时间:

private void test() {
    System.out.println("currentTimeMillis: "+System.currentTimeMillis());
    System.out.println("nanoTime         : "+System.nanoTime());
    System.out.println();

    testNano(false);                                                            // to sync with currentTimeMillis() timer tick
    for(int xa=0; xa<10; xa++) {
        testNano(true);
        }
    }

private void testNano(boolean shw) {
    long strMS=System.currentTimeMillis();
    long strNS=System.nanoTime();
    long curMS;
    while((curMS=System.currentTimeMillis()) == strMS) {
        if(shw) { System.out.println("Nano: "+(System.nanoTime()-strNS)); }
        }
    if(shw) { System.out.println("Nano: "+(System.nanoTime()-strNS)+", Milli: "+(curMS-strMS)); }
    }

4
为了获得更好的游戏图形和平滑的位置更新,请使用System.nanoTime()而不是System.currentTimeMillis()。在我开发的一个游戏中,从currentTimeMillis()切换到nanoTime(),显著提高了动画流畅度。
虽然一毫秒似乎已经很精确了,但在视觉上并非如此。nanoTime()可以改善以下因素:
- 在墙钟分辨率下进行准确的像素定位 - 如果您想要抗锯齿,可以在像素之间进行抗锯齿处理 - Windows墙钟的不准确性 - 时钟抖动(墙钟实际向前滴答声的不一致性)
正如其他答案所建议的那样,如果频繁调用nanoTime会有性能成本--最好每帧只调用一次,并使用相同的值来计算整个帧。

2

System.currentTimeMillis()不适用于测量经过的时间,因为该方法对系统实时时钟的更改非常敏感。 你应该使用System.nanoTime。 请参阅Java System帮助文档:

关于nanoTime方法:

..此方法提供纳秒级精度,但不一定提供纳秒级分辨率(即值更改的频率) - 除了保证分辨率至少与currentTimeMillis()一样好之外,不作任何保证。

如果你使用System.currentTimeMillis(),你的经过时间可能是负数(回到未来)


1

我对nanotime有很好的使用经验。它提供了墙钟时间作为两个长整型(自纪元以来的秒数和该秒内的纳秒数),使用JNI库。它可用于预编译了JNI部分的Windows和Linux。


-3

这里有一件事情需要注意,就是nanoTime方法的不一致性。对于相同的输入,它不能给出非常一致的值。而currentTimeMillis在性能和一致性方面做得更好。虽然它不如nanoTime精确,但误差较小,因此其值更准确。因此,我建议您使用currentTimeMillis。


6
如其他回答和评论所述,currentTimeMillis 受系统时钟变化的影响,因此不适用于计算 JVM 中自某个早期事件以来经过的时间。 - duelin markers

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