Java 计时器领域的现状如何?

Posted

技术标签:

【中文标题】Java 计时器领域的现状如何?【英文标题】:What is the current state of affairs in the world of Java timers? 【发布时间】:2014-09-02 06:33:50 【问题描述】:

我不时遇到提到 System.nanoTime()System.currentTimeMillis() 慢很多(调用可能花费高达微秒),但证明链接经常过时,或者导致一些相当固执的博客文章不能被真正信任,或包含与特定平台有关的信息,或者这个,那个等等。

我没有运行基准测试,因为我对自己进行涉及如此敏感问题的实验的能力持现实态度,但我的条件非常明确,所以我期待一个相当简单的答案。

那么,在平均 64 位 Linux(意味着 64 位 JRE)、Java 8 和现代硬件上,切换到 nanoTime() 会花费我几微秒的时间来调用吗?我应该留在currentTimeMillis()吗?

【问题讨论】:

您需要纳秒精度吗?那你别无选择。如果您不需要准确的时间戳,请不要使用它... 对其进行基准测试应该很容易......只需确保在测量之前预热 JVM。 如果你想现实一点,那么你可能应该做基准测试,因为性能和分辨率really does vary 取决于机器/操作系统。通常,您决定使用其中一个而不是另一个不应由性能决定,因为它们都用于非常不同的情况,即某个分辨率优于另一个。另外,这是similar question。 @BoristheSpider,到目前为止,我什至不需要微秒级精度,但我最近对我的应用程序引擎进行了一些更改(我对此感到非常自豪:))和发现我过去提供的时间戳不够用,即我运行了测试,但由于(我想更深入地了解)低计时器分辨率而失败。所以我决定确保这样的事情不会再发生。 @cdshines 信不信由你,一个月后我现在必须对事件进行高分辨率计时,所以我再次研究了这个问题,这篇文章中从 2003 年开始的方法似乎是其中之一最好的:javaworld.com/article/2077327/core-java/… 【参考方案1】:

与往常一样,这取决于您使用它的目的。由于其他人在抨击nanoTime,我将为其添加一个插件。我在生产代码中专门使用nanoTime 来测量经过的时间

我在生产中回避currentTimeMillis,因为我通常需要一个不会像挂钟那样(并且确实)来回跳动的时钟。这在我使用基于计时器的重要决策的系统中至关重要。 nanoTime 应该以您期望的速度单调递增。

事实上,我的一位同事说“currentTimeMillis 仅对人类娱乐有用”(例如调试日志中的时间,或显示在网站上的时间),因为无法信任它来衡量经过的时间。

但实际上,我们尝试尽可能多地使用时间,并尝试将时间排除在我们的协议之外;然后我们尝试使用逻辑时钟;最后,如果绝对必要,我们使用基于nanoTime 的持续时间。

更新:一个地方我们在连接两台主机时使用currentTimeMillis 作为健全性检查,但我们正在检查主机的时钟是否更多相隔不到 5 分钟。

【讨论】:

谢谢!有不同的观点真的很好。实际上我对这个主题感兴趣的原因是我必须在系统中为事件加上时间戳,并且在某种程度上我根本没有任何问题,但最近我开始注意到有些事件具有相同的currentTimeMillis 时间戳,这在我的情况下是完全不可接受的。这就是为什么我对高分辨率 性能都感兴趣的原因 - 如果两个事件可以在一毫秒内发生,那么使用具有消除性能优势的滞后的方法对它们进行时间戳是没有意义的,IMO。 我的意思是,如果两个事件可以在单个毫秒内发生,则不会以高于毫秒的分辨率获得 实际 时间,但会以几微秒的延迟获得该时间在我的情况下,比获得快速但粗略的结果更重要吗? 是的!对对对!我见过太多的系统失败,因为它们运行在一台时钟会定期自动重置的机器上,而他们在设计时没有考虑到这一点。如果您需要根据实时测量持续时间,则需要一个单调时钟,在 Java 中这意味着 nanotime() 或基于它的东西。 (稍后更正此问题是大型代码库中的一个主要问题——它需要确定哪些 currentTimeMillis() 调用用于持续时间,哪些用于显示挂钟时间,哪些(呃)同时用于两者。 我们从来没有遇到过系统调用的性能妨碍(我们不在内部循环中调用它。)对于像您想要的独特时间戳,现在我们正在转换 @987654329 @ 到微秒,如果它与先前的事件发生冲突,则将其加一。这给了我们 1_000_000 个事件/秒。 您只需要记住,每个主机的 nanoTimes完全不同——它们对于每个 jvm 都是唯一的。【参考方案2】:

在这种情况下,最好的办法始终是对其进行基准测试。而且由于时间完全取决于您的平台和操作系统,因此我们在这里真的无能为力,尤其是因为您无法解释什么您实际使用计时器的目的。

nanoTime 和 currentTimeMillis 通常都不保证单调性(nanoTime 仅适用于 Solaris 的 HotSpot,否则依赖于操作系统的现有单调时间源 - 因此对于大多数人来说,即使 currentTimeMillis 是不是)。

幸运的是,由于 jmh(java 测量工具),这些天你用 Java 编写基准测试相对容易,甚至更幸运的是,Aleksey Shipilёv 不久前实际上调查了nanoTime:See here - 包括做有趣的源代码对自己进行基准测试(这也是 jmh 本身的一个很好的入门读物,如果您想用相对较少的知识编写准确的基准测试 - 那是选择的.. 令人惊讶的是该项目背后的工程师走了多远让基准测试尽可能直接面向普通大众!虽然如果你不小心,你当然仍然可以搞砸;-))

总结现代 linux 发行版或 Solaris 和 x86 CPU 的结果:

精度:30ns 延迟:30ns 最佳情况

窗户:

精度:变化很大,370ns 到 15 µs 延迟:变化很大,15ns 到 15 µs

但请注意,Windows 还可以在某些罕见情况下为您提供高达 100 毫秒 的 currentTimeMillis 精度,因此请选择您的毒药。

Mac OS X:

精度:1µs 延迟:50ns

变化很大,这些结果会因您使用的平台(CPU/MB - 周围有一些有趣的旧硬件组合,但幸运的是它们越来越旧)和操作系统而有很大差异。哎呀,显然只是在 800 MHz CPU 上运行它,与 3.6GHz 服务器相比,你的结果会大不相同。

【讨论】:

【参考方案3】:

运行这个非常简单的测试:

public static void main(String[] args) 
    // Warmup loops
    long l;

    for (int i=0;i<1000000;i++) 
        l = System.currentTimeMillis();
    

    for (int i=0;i<1000000;i++) 
        l = System.nanoTime();
    

    // Full loops
    long start = System.nanoTime();
    for (int i=0;i<10000000;i++) 
        l = System.currentTimeMillis();
    
    start = System.nanoTime()-start;
    System.err.println("System.currentTimeMillis() "+start/1000);

    start = System.nanoTime();
    for (int i=0;i<10000000;i++) 
        l = System.nanoTime();
    
    start = System.nanoTime()-start;
    System.err.println("System.nanoTime() "+start/1000);


在 Windows 7 上,这表明millis 的速度是原来的 2 倍多:

System.currentTimeMillis() 138615
System.nanoTime() 299575

在其他平台上,差异没有那么大,nanoTime() 实际上稍微快一些(~10%):

在 OS X 上:

System.currentTimeMillis() 463065
System.nanoTime() 432896

在带有 OpenJDK 的 Linux 上:

System.currentTimeMillis() 352722
System.nanoTime() 312960

【讨论】:

尽管您从未从局部变量 l 中读取数据,但您可能会发现 JIT 决定优化整个循环... @IanRoberts 我考虑过但不应该,因为它们是方法调用。我只是将测试修改为使用 l 并没有任何区别。 谢谢,我想我会在周一使用我的工作 PC 并在 Linux 平台上进行测量。 Macbook Air, Java7 System.currentTimeMillis() 463065 System.nanoTime() 432896 在 Linux 上使用 OpenJDK 7u55-2.4.7-1ubuntu1 我看到了 System.currentTimeMillis() 352722; System.nanoTime() 312960【参考方案4】:

如果您目前正在使用currentTimeMillis() 并且对分辨率感到满意,那么您绝对不应该更改。

根据javadoc:

此方法提供纳秒级精度,但不一定 纳秒分辨率(即值变化的频率) 不作任何保证,除非决议至少是 和 @link #currentTimeMillis() 一样好。

因此,根据操作系统的实现,不能保证返回的纳米时间是正确的!它只有 9 位数字,与 currentTimeMillis() 的毫秒数相同。

一个完全有效的实现可能是currentTimeMillis() * 1000000

因此,我认为即使没有性能问题,您也不会真正从纳秒中受益。

【讨论】:

此外,nano 在某些虚拟机环境中存在问题,所以除非你真的有理由切换,否则一定要坚持使用currentTimeMillis() @jtahlborn 你想详细说明你的陈述吗? @TimB 我确实回答了“我应该继续使用 currentTimeMillis() 吗?”这个问题。我同意留下来。 @berezovskiy - 一个例子是各种操作系统上的纳米时间依赖于 cpu 计时器。这些计时器应该是单调递增的值。虚拟机主机可以“模拟”具有多个物理 cpu 的单个 cpu,因此可以生成单调递增的纳米时间读数(即它们及时返回或到处倾斜)。 currentTimeMillis() * 1000 只能在微秒内完全有效。 :)【参考方案5】:

我想强调的是,即使调用非常便宜,您也不会获得纳秒级的测量分辨率。

我举个例子(代码来自http://docs.oracle.com/javase/8/docs/api/java/lang/System.html#nanoTime--):

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

因此,虽然两个 long 值都将解析为纳秒,但 JVM 并不能保证每次调用 nanoTime(),JVM 都会给你一个新值。

为了说明这一点,我编写了一个简单的程序并在 Win7x64 上运行(随意运行并报告结果):

package testNano;
public class Main 
    public static void main(String[] args) 
        long attempts = 10_000_000L;
        long stale = 0;
        long prevTime;
        for (int i = 0; i < attempts; i++) 
            prevTime = System.nanoTime();
            long nanoTime = System.nanoTime();
            if (prevTime == nanoTime) stale++;
        
        System.out.format("nanoTime() returned stale value in %d out of %d tests%n", stale, attempts);
    

它打印出nanoTime() returned stale value in 9117171 out of 10000000 tests

编辑

我还建议阅读有关此内容的 Oracle 文章:https://blogs.oracle.com/dholmes/entry/inside_the_hotspot_vm_clocks。文章的结论是:

如果您对测量绝对时间感兴趣,请始终使用 System.currentTimeMillis()。请注意,它的分辨率可能非常粗糙(尽管这在绝对时间内很少成为问题。)

如果您对测量/计算经过的时间感兴趣,请始终使用 System.nanoTime()。在大多数系统上,它会给出微秒级的分辨率。但请注意,此调用在某些平台上也可能需要几微秒才能执行。

您也可能会觉得这个讨论很有趣:Why is System.nanoTime() way slower (in performance) than System.currentTimeMillis()?。

【讨论】:

@TimB,我的意思是没有冒犯,但你有办法提供更好的 - Add Another Answer 按钮就在下方。 @berzovskiy 表示是的,时间不。如果还没有,我稍后再添加。

以上是关于Java 计时器领域的现状如何?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用计时器停止 Java 方法的执行?

如何在java中的单行代码上设置计时器

如何等到计时器在java中停止?

javaWeb中如何做倒计时

在Java中如何实现较为精确的定时器

在java中如何让定时器只启动一次?