我在 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(¤t_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/