Ответ 1
Как указано в комментариях, ScheduledThreadPoolExecutor
основывает свои вычисления на System.nanoTime()
. Что бы там ни было, старый API Timer
предшествовал nanoTime()
и поэтому использует System.currentTimeMillis()
.
Разница здесь может показаться незначительной, но она более значительна, чем можно было ожидать. Вопреки распространенному мнению, nanoTime()
- это не просто "более точная версия" currentTimeMillis()
. Миллис привязан к системному времени, а нанос - нет. Или, как сказано в документации:
Этот метод может использоваться только для измерения прошедшего времени и не связан с каким-либо другим понятием системного или настенного времени. [...] Значения, возвращаемые этим методом, становятся значимыми, только когда вычисляется разница между двумя такими значениями, полученными в одном и том же экземпляре виртуальной машины 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;
Мы видим, что nanos()
использует API QueryPerformanceCounter
, который работает с помощью QueryPerformanceCounter
получая "тики" частоты, определенной QueryPerformanceFrequency
. Эта частота будет оставаться идентичной, но таймер, на котором она основана, и его алгоритм синхронизации, который использует Windows, различаются в зависимости от конфигурации, ОС и аппаратного обеспечения. Даже игнорируя вышеупомянутое, он никогда не будет близок к 100% точности (он основан на достаточно дешевом кварцевом генераторе где-то на плате, а не на стандарте времени Цезия!), Поэтому он будет дрейфовать вместе с системным временем, поскольку NTP сохраняет его синхронно с реальностью.
В частности, эта ссылка дает некоторую полезную информацию и усиливает вышеприведенный вывод:
Если вам нужны метки времени с разрешением 1 микросекунда или более, и вам не нужно синхронизировать метки времени с внешним эталоном времени, выберите QueryPerformanceCounter.
(Болдинг мой.)
Для вашего конкретного случая плохой работы Windows 7 обратите внимание, что в Windows 8+ алгоритм синхронизации TSC был улучшен, и QueryPerformanceCounter
всегда основывался на TSC (в QueryPerformanceCounter
от Windows 7, где это может быть TSC, HPET или Таймер ACPI PM - последний из которых особенно неточен. Я подозреваю, что это наиболее вероятная причина того, что ситуация в Windows 10 значительно улучшается.
При этом вышеперечисленные факторы по-прежнему означают, что вы не можете полагаться на ScheduledThreadPoolExecutor
чтобы идти в ногу с "реальным" временем - оно всегда будет дрейфовать. Если этот дрейф является проблемой, то это не решение, на которое вы можете положиться в этом контексте.
Примечание: в Windows 8+ есть функция GetSystemTimePreciseAsFileTime
которая предлагает высокое разрешение QueryPerformanceCounter
сочетании с точностью системного времени. Если бы Windows 7 была отброшена как поддерживаемая платформа, теоретически это можно было бы использовать для предоставления метода System.getCurrentTimeNanos()
или аналогичного, предполагая, что существуют другие аналогичные собственные функции для других поддерживаемых платформ.