我在Windows 7上运行Java服务,该服务每天在上运行一次SingleThreadScheduledExecutor。尽管它不是很关键,但我从来没有给过太多,但最近查看了一下数据,发现该服务每天大约漂移15分钟,这听起来实在太多了。
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
long drift = (System.currentTimeMillis() - lastTimeStamp - seconds * 1000);
lastTimeStamp = System.currentTimeMillis();
}, 0, 10, TimeUnit.SECONDS);
Run Code Online (Sandbox Code Playgroud)
这种方法+110ms每10秒几乎都会漂移一次。如果我每隔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);
Run Code Online (Sandbox Code Playgroud)
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则不受限制。或者如文档所述:
此方法只能用于测量经过的时间,与系统或挂钟时间的任何其他概念无关。[...] 通过该方法返回的值成为只有当一个Java虚拟机的同一实例中获得的两个这种值之间的差,计算有意义的。
在您的示例中,您没有遵循此指导来使值“有意义”-可以理解,因为这些值ScheduledThreadPoolExecutor仅用nanoTime()作实现细节。但是最终结果是相同的,那就是您不能保证它将与系统时钟保持同步。
但是为什么不呢?秒是秒,对吧,所以两者应该从某个已知点保持同步?
好吧,从理论上讲,是的。但实际上,可能并非如此。
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;
Run Code Online (Sandbox Code Playgroud)
我们看到nanos()使用了QueryPerformanceCounterAPI,该API通过QueryPerformanceCounter获取定义的频率的“滴答声” 而起作用QueryPerformanceFrequency。该频率将保持不变,但是它基于的计时器以及Windows使用的同步算法根据配置,操作系统和底层硬件而有所不同。即使忽略以上内容,它也永远不会接近100%的准确度(它是基于电路板上某处相当便宜的晶体振荡器,而不是铯时间标准!),因此它将随着系统时间而漂移,因为NTP保持了它与现实同步。
特别是,此链接提供了一些有用的背景,并加强了上面的内容:
当您需要分辨率为1微秒或更佳的时间戳,并且不需要将时间戳同步到外部时间参考时,请选择QueryPerformanceCounter。
(粗体是我的。)
对于Windows 7的您的具体情况差强人意,请注意在Windows 8+的TSC同步算法进行了改进,并QueryPerformanceCounter在始终根据TSC(如反对到Windows 7,它可能是一个TSC,HPET或者ACPI PM计时器-后者尤其不准确。)我怀疑这是Windows 10上这种情况得到极大改善的最可能原因。
话虽如此,上述因素仍然意味着您不能依赖于ScheduledThreadPoolExecutor与“真实”时间保持一致-它总是会漂移的。如果这种漂移是一个问题,那么这不是您可以在此情况下依赖的解决方案。
旁注:在Windows 8+中,有一项GetSystemTimePreciseAsFileTime功能可以提供高分辨率和QueryPerformanceCounter系统时间精度。如果将Windows 7删除为受支持的平台,则在理论上可以用来提供一种System.getCurrentTimeNanos()方法或类似方法,前提是其他受支持的平台还存在其他类似的本机功能。