为什么 Java Scheduler 在 Windows 上表现出明显的时间漂移?

2022-09-01 14:40:10

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

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

这种方法每10秒漂移一致。如果我以1秒的间隔运行它,漂移平均值。+110ms+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:使用Experidor像疯了一样漂移,与Timer一起漂移

使用 Java8 和 Java11 进行了测试。

有趣的是,如果你假设每秒漂移11毫秒,你每天会得到950400毫秒的漂移,相当于每天。所以它非常一致。15.84 minutes

问题是:为什么?
为什么单线程执行器会发生这种情况,而计时器则不会发生这种情况。

更新1:按照Slaw的评论,我在多个不同的硬件上尝试过。我发现这个问题在任何个人硬件上都没有表现出来。只在公司一个。在公司硬件上,它也体现在Win10上,尽管要少一个数量级。


答案 1

正如注释中指出的那样,其计算基于 。无论好坏,旧的API都早于,因此使用。ScheduledThreadPoolExecutorSystem.nanoTime()TimernanoTime()System.currentTimeMillis()

这里的差异可能看起来很微妙,但比人们想象的要重要得多。与普遍的看法相反,它不仅仅是一个“更准确的版本”。Millis被锁定到系统时间,而nanos则不是。或者正如文档所说nanoTime()currentTimeMillis()

此方法只能用于测量经过的时间,与任何其他系统或挂钟时间概念无关。[...]仅当计算了在 Java 虚拟机的同一实例中获得的两个此类值之间的差异时,此方法返回的值才有意义。

在您的示例中,您没有遵循此指南以使值“有意义” - 可以理解,因为唯一用作实现详细信息。但最终结果是相同的,即您无法保证它将与系统时钟保持同步。ScheduledThreadPoolExecutornanoTime()

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

好吧,从理论上讲,是的。但在实践中,可能不是。

看看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;

我们看到使用API,它的工作原理是获取由 定义的频率的“刻度”。该频率将保持不变,但它所基于的计时器以及Windows使用的同步算法因配置,操作系统和底层硬件而异。即使忽略上述内容,它也永远不会接近100%准确(它基于电路板上某个地方相当便宜的晶体振荡器,而不是铯时间标准!),因此它将随着NTP保持与现实同步而随系统时间漂移。nanos()QueryPerformanceCounterQueryPerformanceCounterQueryPerformanceFrequency

特别是,这个链接提供了一些有用的背景,并加强了上面的桥:

如果需要分辨率为 1 微秒或更高的时间戳,并且不需要将时间戳同步到外部时间参考,请选择“查询性能计数器”。

(粗体是我的。

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

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

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


答案 2

CronScheduler是我的一个项目,旨在证明时间漂移问题,同时它避免了本文中描述的旧类的一些问题。Timer

用法示例:

Duration syncPeriod = Duration.ofMinutes(1);
CronScheduler cron = CronScheduler.create(syncPeriod);
cron.scheduleAtFixedRateSkippingToLatest(0, 1, TimeUnit.MINUTES, runTimeMillis -> {
    // Collect and send summary metrics to a remote monitoring system
});

注意:这个项目实际上是受这个StackOverflow问题的启发。


推荐