Почему планировщик Java демонстрирует значительный временной сдвиг в Windows? - PullRequest
15 голосов
/ 13 июня 2019

У меня есть служба Java, работающая в Windows 7, которая запускается один раз в день на SingleThreadScheduledExecutor. Я никогда не давал это много, хотя это не критично, но недавно посмотрел на цифры и увидел, что сервис дрейфовал примерно 15 минут в день, что звучит слишком много, так что откопал его.

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

Этот метод довольно последовательно дрейфует +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);

Linux: не дрейфует (ни с Executor, ни с Timer)
Windows: с Executor сходит с ума, с Timer - нет

Протестировано с Java8 и Java11.

Интересно, что если вы допустите дрейф 11 мс в секунду, вы получите дрейф 950400 мс в день, что составляет 15.84 minutes в день. Так что это довольно последовательно.

Вопрос: почему?
Почему это происходит с SingleThreadExecutor, но не с таймером.

Обновление 1: после комментария Слава Я пробовал на нескольких разных аппаратных средствах. Я обнаружил, что эта проблема не проявляется ни на одном персональном оборудовании. Только на компанию одну. На оборудовании компании это также проявляется на Win10, хотя на порядок меньше.

1 Ответ

7 голосов
/ 14 июня 2019

Как указано в комментариях, ScheduledThreadPoolExecutor основывает свои расчеты на System.nanoTime().Что бы там ни было, старый Timer API, однако, предшествовал nanoTime(), и поэтому вместо него используется System.currentTimeMillis().

Разница здесь может показаться незначительной, но более существенной, чем можно было ожидать.Вопреки распространенному мнению, nanoTime() является , а не просто «более точной версией» currentTimeMillis(). Миллис привязан к системному времени, а нанос - нет. Или , как написано в документах :

Этот метод может использоваться только для измерения прошедшего времении не связан с каким-либо другим понятием системного или настенного времени.[...] Значения, возвращаемые этим методом, становятся значимыми, только когда вычисляется разница между двумя такими значениями, полученными в одном и том же экземпляре виртуальной машины Java.

В вашем примере вы не следуете этому руководству, чтобы значения были "значимыми" - понятно, потому что ScheduledThreadPoolExecutor использует только nanoTime() в качестве детали реализации.Но конечный результат тот же, что вы не можете гарантировать, что он будет синхронизирован с системными часами.

Но почему бы и нет?Секунды - это секунды, верно, поэтому они должны оставаться синхронизированными с определенной известной точки?

Ну, в теории да.Но на практике, вероятно, нет.

Взгляните на соответствующий нативный код в 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, который работает путем 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() или аналогичного, предполагая, что существуют другие аналогичные собственные функции для других поддерживаемых платформ.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...