Вимірювання часу та засипання потоків, Java Specialist

Блог про пам'ять, збирача сміття, багатопоточність і продуктивність в java

Зміст

Вимірювання часу та засипання потоків

У даному топіку я хотів би поговорити про інструментарій доступний в java для вимірювання часу і запуску таймерів, про їхню точність, продуктивність і можливі проблеми при роботі з ними. Наприклад, на Windows при певних патернах роботи з Thread.sleep() системний годинник може почати йти помітно швидше, додаючи по кілька секунд на кожній годині. Також можна зіткнутися з великим джиттером під час роботи з таким класом як ScheduledThreadPoolExecutor.

System.currentTimeMillis()
System.nanoTime()

Другий метод використовує спеціальні лічильники, не пов'язані з системним годинником (хоча на деяких платформах він і може бути реалізований через них, але це швидше виняток ніж правило). Формально System.nanoTime() повертає наносекунди, але послідовні виклики цього методу навряд чи дадуть точність більше мікросекунд. На більшості систем цей метод повертатиме незменшувані значення, для чого йому може знадобитися внутрішня синхронізація, якщо він буде викликатися на різних процесорах. Тому продуктивність цього методу дуже залежить від заліза, і на деяких машинах запит до цього методу може легко займати більше часу, ніж запит до System.currentTimeMillis() [2].

Враховуючи відносність часу, що повертається даним методом, його неможливо використовувати, скажімо, для вимірювання часу передачі повідомлення від одного боксу до іншого. Хоча звичайно можна виміряти час повного round-trip, відняти час проведений на другій машині і поділити на два. Однак якщо у вас розподілена програма і вам дуже важливо міряти час витрачений напевних операціях, які розподілені з різних боксів, то ви можете написати нативний метод, який повертатиме абсолютний час з більшою точністю. Я бачив такий підхід до одного з проектів, з якими мені доводилося інтегруватися.

Thread.sleep() / Object#wait()

За допомогою цих методів можна попросити поточний потік заснути на певну кількість мілісекунд. Точність прокидання залежатиме від розміру інтервалу переривань на ОС. На Windows це зазвичай 10 мс (але на деякому залізі може бути й 15 мс [4]). Однак, довжина цього інтервалу може бути змінена навіть стандартними засобами java. Це перемикання відбувається автоматично, якщо ви просите заснути будь-який потік на час не кратний, поточний інтервал переривань. Причому коли цей потік прокинеться, ОС повернеться назад до штатного режиму.

Однак з цим треба бути дуже акуратним, оскільки часте перемикання між цими режимами через баг у Windows [6] може викликати зміну в нормальному ході системного годинника. Я одного разу зіштовхнувся зі скаргами клієнтів однієї з додатків над якими я працював, що коли вони запускають наш продукт, то їх годинник починає поспішати на кілька секунд на годину. Особливо ніхто не звертав на ці скарги увагу, тому що не особливо розуміли, як це взагалі може відбуватися. Як виявилося, це дійсно може мати місце, якщо Windows часто перемикається між режимами з різною точністю інтервалів переривання. Зробити це досить просто, достатньо запустити у тлі якийсь потік, який у циклі буде викликати Thread.sleep() , передаючи як параметр невелике число не кратне 10 мс. Наприклад, 1, 5 або 25. Найцікавіше, що це перемикання відбудеться навіть якщо ви попросите потік заснути на 1001 мс, що вже здавалося бБезглуздим, зате це дає дуже витончений workaround, описаний нижче. Але варто зауважити, що при виклику sleep() на тривалий час перемикання режимів відбувається не часто і проблема з поспіхом системного годинника виявлятися не буде. Ще на javamex [5] пишуть, що навіть при дефолтному періоді переривань 15 мс, JVM вважає, що дефолтне значення 10 і може перейти в режим підвищеної точності переривань при спробі засипання на 15 мс.

Щоб обійти баг з поспіхом годинника в JVM був зроблений прапор -XX:+ForceTimeHighResolution, який повинен був на старті JVM переводити систему в режим підвищеної точності переривань. Але через бага в його імплементації [3], вийшло, що він робить зовсім інше, а саме заморожує штатну довжину переривання і ніякі команди sleep() її вже не поміняють. Що втім теж стало вирішенням проблеми відхилення в ході системного годинника. Кумедно, але офіційна відповідь на баг у реалізації даного прапора полягає в тому, що його правити не будуть, оскільки, по-перше, він вирішує початкову проблему, по-друге, він був впроваджений дуже давно і багато хто вже розраховує на те, як він працює зараз, і, по-третє, існує витончений workaround, який дозволяє зробити саме те, для чого спочатку задумався цей прапор.