我在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测试。

有趣的是,如果您假设每秒11ms的漂移,那么您每天将获得950400ms的漂移,相当于每天15.84 minutes。因此,这是非常一致的。

问题是:为什么?
为什么使用SingleThreadExecutor而不是使用Timer会发生这种情况。

Update1:​​在Slaw评论之后,我尝试了多种不同的硬件。我发现此问题在任何个人硬件上均未发现。仅对公司一。在公司硬件上,它也出现在Win10上,尽管数量少了一个数量级。

最佳答案

正如评论中指出的那样,ScheduledThreadPoolExecutor的计算基于System.nanoTime()。不管是好是坏,旧的Timer API都在nanoTime()之前,因此改用System.currentTimeMillis()

这里的差异可能看起来微妙,但比人们预期的更重要。与流行的看法相反,nanoTime()不仅仅是currentTimeMillis()的“更准确的版本”。 Millis被锁定为系统时间,而nanos则未锁定。 as the docs put it:



在您的示例中,您没有遵循该指导来使值“有意义”-可以理解,因为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,该API通过QueryPerformanceCounter获得QueryPerformanceFrequency定义的频率的“滴答声”来工作。该频率将保持不变,但是它基于的计时器以及Windows使用的同步算法根据配置,操作系统和底层硬件而有所不同。即使忽略以上内容,它也永远不会接近100%的准确度(它是基于电路板上某处相当便宜的晶体振荡器,而不是铯时间标准!)因此,随着NTP的发展,它会随着系统时间而漂移。与现实同步。

特别地,this link提供了一些有用的背景,并加强了上面的内容:



(粗体是我的。)

对于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 Scheduler在Windows上会出现明显的时间漂移​​?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/56571647/

10-13 01:01