Времеви и спящи нишки, Java специалист
Блог за памет, събирач на отпадъци, многопоточност и производителност в java
Съдържание
Измерване на време и спящи нишки
В тази тема бих искал да говоря за инструментите, налични в java за измерване на време и стартиране на таймери, за тяхната точност, производителност и възможни проблеми при работа с тях. Например, в Windows, с определени модели на работа с Thread.sleep(), системният часовник може да започне да върви значително по-бързо, добавяйки няколко секунди към всеки час. Можете също така да попаднете на много трептене, когато работите с клас като ScheduledThreadPoolExecutor.
System.currentTimeMillis()
System.nanoTime()
Вторият метод използва специални броячи, които не са свързани със системния часовник (въпреки че на някои платформи може да се реализира чрез тях, но това е по-скоро изключение, отколкото правило). Формално System.nanoTime() връща наносекунди, но последователните извиквания на този метод е малко вероятно да дадат точност, по-голяма от микросекунди. В повечето системи този метод ще върне ненамаляващи стойности, което може да изисква вътрешна синхронизация, ако се извика на различни процесори. Следователно производителността на този метод зависи много от хардуера и на някои машини извикването на този метод може лесно да отнеме повече време от извикването на System.currentTimeMillis() [2].
Като се има предвид относителността на времето, върнато от този метод, той не може да се използва, да речем, за измерване на времето, необходимо за изпращане на съобщение от една кутия в друга. Въпреки че, разбира се, можете да измерите времето за пълно пътуване в двете посоки, извадете времето, прекарано на втората машина, и разделете на две. Ако обаче имате разпределено приложение и за вас е много важно да измервате времето, прекарано в негоопределени операции, които са разпределени в различни кутии, тогава можете да напишете собствен метод, който ще върне абсолютното време с по-голяма точност. Видях този подход в един от проектите, с които трябваше да се интегрирам.
Thread.sleep() / Object#wait()
Използвайки тези методи, можете да помолите текущата нишка да заспи за определен брой милисекунди. Точността на събуждането ще зависи от размера на интервала на прекъсване на вашата операционна система. В Windows това обикновено е 10 ms (но на някои хардуери може да е 15 ms [4]). Дължината на този интервал обаче може да бъде променена дори от стандартна java. Това превключване се извършва автоматично, ако помолите някоя нишка да заспи за време, което не е кратно на текущия интервал на прекъсване. Освен това, когато тази нишка се събуди, операционната система ще се върне обратно към нормален режим.
Трябва обаче да бъдете много внимателни с това, тъй като честото превключване между тези режими поради грешка в Windows [6] може да доведе до промяна в нормалния ход на системния часовник. Веднъж се сблъсках с оплаквания от клиенти на едно от приложенията, върху които работих, че когато стартират нашия продукт, часовниците им започват да се ускоряват с няколко секунди на час. Никой не обърна особено внимание на тези оплаквания, защото не разбираха как изобщо може да се случи това. Както се оказва, това всъщност може да се случи, ако Windows често превключва между режими с различна прецизност на интервала на прекъсване. Много е лесно да направите това, достатъчно е да стартирате някаква нишка във фонов режим, която ще извика Thread.sleep() в цикъл, предавайки като параметър малко число, некратно на 10 ms. Например 1, 5 или 25. Смешното е, че това превключване ще се случи, дори ако поискате нишката да заспи за 1001 ms, което вече изглеждабезсмислено, но дава много елегантно решение, описано по-долу. Но си струва да се отбележи, че при извикване на заспиване () за дълго време, превключването на режима не се случва често и проблемът с бързането на системния часовник няма да се появи. Те също така пишат на javamex [5], че дори при период на прекъсване по подразбиране от 15 ms, JVM счита, че стойността по подразбиране е 10 и може да превключи към режим с висока точност на прекъсване, когато се опитва да заспи за 15 ms.
За да се заобиколи грешката с часовниковата скорост в JVM, беше въведен флагът -XX:+ForceTimeHighResolution, който трябваше да постави системата в режим на висока точност на прекъсване в началото на JVM. Но поради бъг в реализацията му [3] се оказа, че той прави нещо съвсем различно, а именно замразява стандартната дължина на прекъсването и никакви sleep() команди няма да я променят. Което между другото също беше решение на проблема с отклоненията в хода на системния часовник. Смешно е, но официалният отговор на грешката в внедряването на този флаг е, че той няма да бъде коригиран, защото, първо, решава първоначалния проблем, второ, беше въведен отдавна и мнозина вече разчитат как работи сега, и трето, има елегантно решение, което ви позволява да правите точно това, за което първоначално е предназначен този флаг.