首页  >  文章  >  为什么在大时间窗口上计算的经过时间在 System.currentTimeMillis 与 System.nanoTime 之间有高达 100+ 毫秒的差异

为什么在大时间窗口上计算的经过时间在 System.currentTimeMillis 与 System.nanoTime 之间有高达 100+ 毫秒的差异

王林
王林转载
2024-02-09 10:24:08873浏览

php小编苹果为大家解答为什么在大时间窗口上计算的经过时间在 System.currentTimeMillis 与 System.nanoTime 之间会有高达 100+ 毫秒的差异。这个问题涉及到计算机系统的底层机制和操作系统的调度方式。在计算经过时间时,System.currentTimeMillis 使用的是操作系统提供的墙钟时间,而System.nanoTime 使用的是CPU时钟周期。由于操作系统的调度机制和硬件时钟的异步性,这两种计时方法会导致时间差异。具体的原因和解决方案将在接下来的文章中详细介绍。

问题内容

我正在尝试创建一个可在同一 jvm 中进行微秒进动排序的 ksuid,因为 currenttimemillis 仅给出毫秒精度,考虑将 currenttimemillis() 和 nanotime() 一起使用,收集第一个瞬间和第二个瞬间开始的值计算自第一个时间瞬间以来经过的微秒数的 nanotime 差异。使用从 nanotime 计算出的经过时间并将其添加到初始即时毫秒中,以便我们可以在 jvm 启动后的任何即时时间(均在同一 jvm 内)以微秒精度计算 epoch。

//静态收集初始nanotime和currentmillis 最终长 inittimenanos = system.nanotime();最终长 inittimemillis = system.currenttimemillis();

//从这里开始,当需要微/纳秒精度的当前时间时,从nanotime计算经过的时间并将经过的毫秒添加到initialmillis,剩下的经过时间给出微秒精度的时间

final long elapsednanotime = system.nanotime() - inittimenanos;最终双精度 elapsedmillisfromnanotime = elapsednanos / (1000000.0);

//自jvm启动以来单调递增的时间戳毫秒(epoch,我们可能不称其为epoch) final 长计算当前时间millis = inittimemillis + elapsedmillisfromnanotime; final long nanosprecision = elapsednanos % 1000000; //来自nanotime的附加时间精度

考虑使用这些值来生成单调递增的 ksuid,它可以近似表示当前时间,但保证根据同一 jvm 内的创建时间进行排序,因为 currenttimemillis 不提供单调递增的时间戳保证,考虑使用这种方法生成大约接近真实时间戳的单调递增时间戳(可能有几毫秒的差异,除非对纪元时间进行闰秒调整)。使用 epoch 和 nanotime 计算出的经过时间预计有几毫秒的差异,但实际差异非常频繁地变化,当我在测试以下运行 48 小时时,观察到这两者之间的差异高达 150 毫秒。大多数情况下,使用 nanotime 计算的经过时间高于使用 currenttimemillis 计算的经过时间,并且观察到的时间范围为 -2 毫秒到 +150 毫秒。

final long elapsedmillis = system.currenttimemillis() - inittimemillis; //来自system.currenttimemillis()的经过时间毫秒 最终双方差millis = elapsedmillisfromnanotime - elapsedmillis; //经过时间方差

我是否遗漏了有关 jvm 时间保证的任何内容,计算单调递增近似时间戳的方法是否错误?(这不会用作系统中的真实纪元,仅用于生成 uuid,uuid 也代表同一 jvm 内的近似时间戳即时)。

//测试类

package org.example;

import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) throws Exception {
        final long initTimeNanos = System.nanoTime();
        final long initTimeMillis = System.currentTimeMillis();
        System.out.println("Nanos: " + initTimeNanos);
        System.out.println("Millis: " + initTimeMillis);

        while (true) {
            final long currentNanos = System.nanoTime();
            final long elapsedNanos = currentNanos - initTimeNanos;
            final double elapsedMillisFromNanos = elapsedNanos / 1000000.0;
            final long elapsedMillis = System.currentTimeMillis() - initTimeMillis;
            final double varianceMillis = elapsedMillisFromNanos - elapsedMillis;
            if (Math.abs(varianceMillis) > 1) {
                System.out.printf("\nVariance Observed: %.6f\n", varianceMillis);
                System.out.printf("Elapsed Time: %.6fms (from System.nanoTime)\n", elapsedMillisFromNanos);
                System.out.printf("Elapsed Time: %dms (from System.currentTimeMillis)\n", elapsedMillis);
            }
            if (elapsedMillis > TimeUnit.HOURS.toMillis(48)) {
                break;
            }
            Thread.sleep(5000);
            System.out.print(".");
        }
    }
}

为什么经过的时间方差不断变化? 如果 jvm 连续运行一年,我们可以预期的最大方差是多少(任何 jvm 对此上限或下限的保证,使用 mac 和 windows 进行测试,mac 给出了方差的缓慢增长,windows 的速度要快得多)?

我预计经过的时间变化小于 10 毫秒,变化变化频率较低。 但实际观察是方差不断变化,上下波动,48小时内观察到最多150毫秒

解决方法

一种解释是基于 ntp 的时间涂抹。更一般地说,纳米时间和 ctm 测量完全不同的东西,你不能将它们混合。

nanotime 有一个任意的 0 点(获得 0 的 nanotime 没有特殊意义),因此,除了将它返回的内容与不同 nanotime 调用的结果进行比较之外,根本没有必要调用它。 nanotime 跟踪经过的时间,就是这样。

system.ctm 获取系统时钟。如果您使用 posix date 命令或在系统设置中编辑系统的时间设置,则不会影响 nanotime,但会更改 system.ctm 返回的内容。 ctm 通常也比 nanotime 慢得多。 ctm 还有一个明确定义的 0 - 表示 utc 1970 年 1 月 1 日午夜。

问题:“我希望当前时间符合纳秒精度的系统时钟”在 jvm 上不可能

时间涂抹是指某些网络时间守护程序注意到您的系统时钟略有偏差,并且不只是将系统时钟编辑到正确的时间,而是引入了涂抹:假设您比实时时间“提前”了 400 毫秒。 ntp 可以将您的时钟向后设置 400 毫秒,但许多日志记录系统假设 system.ctm 不会向后移动(这是一个不正确但广泛应用的假设)。

这可以通过让时间放慢一段时间来“修复”:每经过 100 毫秒,ntp 守护进程就会将时钟“保持”在同一毫秒上一毫秒。每 100 毫秒就赶上 1 毫秒,因此在 400,000 毫秒(仅 400 秒)内,时钟恢复与网络同步,并且日志记录根本不受影响。

但是,这显然会完全破坏 system.ctm 和 nanotime 之间的任何关系!

大多数 ntp 都是这样涂抹的(它们也会向前涂抹 - 如果您的系统时钟落后,它不仅仅会向前跳跃:这会使日志撒谎(使其看起来就像两个事件之间存在一些间隙)比实际大得多),因此每 100 毫秒,ntp 就会使时钟跳过一毫秒,类似这样的事情,以赶上。

...但我需要单调递增的时间戳!

那么 nanotime 就无关紧要了。不要使用它。

拥有一些提供 id 的集中“商店”。一种实现:

class TimeStore {
  long lastVal = 0L;

  public synchronized long get() {
    long mark = System.currentTimeMillis() << 4;
    return lastVal = Math.max(mark, lastVal + 1);
  }
}

这将返回当前时间,左移 4 位,并将填充此移位“释放”的 16 个值,以便能够在同一时间生成单调递增值,最多 16 次;同一毫秒内的任何进一步请求都会潜入下一毫秒。

尽管如此,这可能并不比nanotime慢。

rzwitserloot的回答是正确的。我将向您提供我对各种问题的看法。

currenttimemillisnanotime 无关

system.currenttimemillissystem.nanotime 彼此无关。

  • currenttimemillis 从主机的硬件时钟单元检索当前日期和时间,由主机操作系统管理。
  • nanotime 来自主机 cpu 保存的计数。

currenttimemillis

所以首先要了解人类的年月日和时分秒的概念。传统计算机中使用的时钟硬件的分辨率有限,有些是毫秒,有些是微秒,但没有纳秒。

由于各种原因,本次通话报告的日期和时间可能会有所不同。其中一个原因是,在电池电量耗尽的情况下启动的计算机会将其时钟重置为默认时刻,直到通过调用时间服务器进行纠正。另一个原因是系统管理员或用户可能会更改日期时间。还有一个原因是硬件时钟可能无法很好地保持时间,并且会通过调用时间服务器在更新之间运行得快或慢。

nanotime

nanotime 调用以纳秒计数形式告知经过的时间。但这个流逝的时间与日历和墙上的时钟无关。此调用仅返回经过的纳秒的单调计数。

这个计数非常准确,因为它来自计算机的“跳动的心脏”。返回的数量始终在增加,直到达到 long.max_value,然后环绕到 long.min_value。这给出了大约 292 年的范围,但这并不意味着从现在起 292 年。计数的起点未指定。在某些 java 实现中,您可能会看到计数在计算机启动时开始。但并不能保证这一点。

问题

您可能会考虑寻找 新 6、7、8 版本 的实现a href="https://en.wikipedia.org/wiki/universally_unique_identifier" rel="nofollow noreferrer">uuid 被提议给 ietf 进行标准化。

currenttimemillis 在现代 java 中已被 java.time.instant 类取代。调用 instant.now 捕获 utc 中的当前时刻。

java 9+ 中的常见实现以微秒为单位报告,但在 java 8 中以毫秒为单位。 instant 类能够以纳秒为单位。

聪明的主意。但不现实。如上所述,currenttimemillisnanotime 不相关。最重要的是,currenttimemillis 的结果可能会因多种原因而有所不同。同时,nanotime 计数可能会随着主机的每次启动而变化,并且肯定不会对应于其他计算机上进行的任何 nanotime 调用。

是的,您忽略了这样一个事实:java 规范没有做出您似乎假设的任何保证。

javadoc 中唯一的保证是 nanotime 是“高分辨率”,至少与 currenttimemillis 一样好。并且无法保证您预计何时能迎来 292 年的终结。

同时,currenttimemillis是一个移动目标,可以随时改变,向前或向后移动。

这是不可预测的。

java 规范没有做出这样的保证。

以上是为什么在大时间窗口上计算的经过时间在 System.currentTimeMillis 与 System.nanoTime 之间有高达 100+ 毫秒的差异的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文转载于:stackoverflow.com。如有侵权,请联系admin@php.cn删除