java - 为什么 Java 调度程序在 Windows 上表现出明显的时间漂移​​?

标签 java time

我在 Windows 7 上运行 Java 服务,该服务每天在 SingleThreadScheduledExecutor 上运行一次。我从来没有给它太多,因为它不重要,但最近查看了数字,发现该服务每天漂移大约 15 分钟,这听起来太多了,所以把它挖出来了。

Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
   long drift = (System.currentTimeMillis() - lastTimeStamp - seconds * 1000);
   lastTimeStamp = System.currentTimeMillis();
}, 0, 10, TimeUnit.SECONDS);

此方法非常一致地每 10 秒漂移 +110ms。如果我以 1 秒的间隔运行它,漂移平均值为 +11ms

有趣的是,如果我对 Timer() 执行相同的操作,值会与小于整整毫秒的平均漂移非常一致。

new Timer().schedule(new TimerTask() {
    @Override
    public void run() {
        long drift = (System.currentTimeMillis() - lastTimeStamp - seconds * 1000);
        lastTimeStamp = System.currentTimeMillis();
    }
}, 0, seconds * 1000);

Linux: 不会漂移(对于 Executor 和 Timer 也是如此)
Windows:与 Executor 一起疯狂漂移,与 Timer 无关

使用 Java8 和 Java11 测试。

有趣的是,如果您假设漂移为每秒 11 毫秒,那么每天漂移 950400 毫秒,相当于每天 15.84 分钟。所以它非常一致。

问题是:为什么?
为什么这会发生在 SingleThreadExecutor 而不是 Timer 上。

更新 1:根据 Slaw 的评论,我尝试了多种不同的硬件。我发现这个问题不会出现在任何个人硬件上。只在公司一个。在公司硬件上,它也出现在 Win10 上,尽管数量级要少。

最佳答案

正如评论中所指出的,ScheduledThreadPoolExecutor 的计算基于 System.nanoTime()。不管是好是坏,旧的 Timer API 然而先于 nanoTime(),因此使用 System.currentTimeMillis() 代替。

这里的区别可能看起来很微妙,但比人们想象的要重要得多。与流行的看法相反,nanoTime() 只是 currentTimeMillis() 的“更准确版本”。 Millis 锁定到系统时间,而 nanos 不是。 或者 as the docs put it :

This method can only be used to measure elapsed time and is not related to any other notion of system or wall-clock time. [...] The values returned by this method become meaningful only when the difference between two such values, obtained within the same instance of a Java virtual machine, is computed.

在你的例子中,你没有遵循这个指导值是“有意义的” - 可以理解,因为 ScheduledThreadPoolExecutor 只使用 nanoTime() 作为实现细节。但最终结果是一样的,那就是你不能保证它会与系统时钟保持同步。

但为什么不呢?秒就是秒,对吧,所以两者应该从某个已知点保持同步?

嗯,理论上,是的。但在实践中,可能不会。

Taking a look at the relevant native code on windows :

LARGE_INTEGER current_count;
QueryPerformanceCounter(&current_count);
double current = as_long(current_count);
double freq = performance_frequency;
jlong time = (jlong)((current/freq) * NANOSECS_PER_SEC);
return time;

我们看到 nanos() 使用 QueryPerformanceCounter API,它通过 QueryPerformanceCounter 获取由 查询性能频率。该频率将保持不变,但它所基于的计时器以及 Windows 使用的同步算法因配置、操作系统和底层硬件而异。即使忽略上述内容,它也永远不会接近 100% 准确(它基于板上某处的相当便宜的晶体振荡器,而不是 Cesium 时间标准!)因此它会随着系统时间漂移,因为 NTP 使其与现实保持同步。

特别是,this link给出了一些有用的背景,并加强了上述观点:

When you need time stamps with a resolution of 1 microsecond or better and you don't need the time stamps to be synchronized to an external time reference, choose QueryPerformanceCounter.

(粗体是我的。)

对于 Windows 7 性能不佳的具体情况,请注意,在 Windows 8+ 中,TSC 同步算法得到改进,QueryPerformanceCounter 始终基于 TSC(与 Windows 7 相反,它可能是 TSC、HPET 或 ACPI PM 计时器 - 后者尤其不准确。)我怀疑这是 Windows 10 上情况大幅改善的最可能原因。

话虽如此,上述因素仍然意味着您不能依赖 ScheduledThreadPoolExecutor 来跟上“实时”时间 - 它总是会漂移。如果这种漂移是一个问题,那么在这种情况下它不是您可以依赖的解决方案。

旁注:在 Windows 8+ 中,有一个 GetSystemTimePreciseAsFileTime function它提供了 QueryPerformanceCounter 的高分辨率以及系统时间的准确性。如果 Windows 7 不再作为受支持平台,这在理论上可用于提供 System.getCurrentTimeNanos() 方法或类似方法,前提是其他受支持平台存在其他类似的 native 函数。

关于java - 为什么 Java 调度程序在 Windows 上表现出明显的时间漂移​​?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56571647/

相关文章:

c++ - `clock()` 给出通常的时钟而不是 CPU 时钟

java - 一个生产者和多个消费者的生产者消费者

time - 内置@time 宏与基准模块中的宏之间的区别

java - java中的时间和小时

java - 如何同时打开两个JFrame?

javascript - 每秒更新 Javascript 中的倒计时时间

bash -/usr/bin/time --format 以毫秒为单位输出耗时

java - 在 Android 中使用 RecyclerView 不显示列表

java - Spring 启动1.5.2.RELEASE : Rolling File logging

java - "No Main Manifest Attribute"在 -----.jar Netbeans