Изявление d Бележки на програмиста за рейк и примери
Това е страхотно!
java.lang.Състояния на нишки в рейк и примери
Всеки java разработчик знае какво е нишка, как да я стартира и евентуално да промени приоритета й или дори да я направи демон. Днес тези повърхностни познания често са достатъчни, за да се справят успешно с ежедневните си задачи, в които готините рамки правят всичко възможно да скрият от нас нюансите на многонишковостта. Но понякога животът ви принуждава да слезете до дъното на по-ниско ниво и да се запознаете по-подробно с нюансите на работа с потоци.
В тази статия, докато решаваме прости проблеми, чрез поредица от проби и грешки, ще разгледаме някои от нюансите при работа с класа Thread в java, ще говорим за това какви нишки имат състояния и при какви условия една нишка преминава от едно състояние в друго.
Проблем с ходещ робот
Необходимо е да се напише робот, който може да ходи. За движението на всеки от краката му отговаря отделна нишка. Стъпката се изразява в изход към конзолата НАЛЯВО или НАДЯСНО.
Вариант първи: споделено състояние.
Ако стартирате този код, ще откриете, че разходката на нашия робот няма да е толкова дълга, колкото искахме, а в някои случаи дори може да бъде ограничена до една единствена стъпка.
Това се дължи на факта, че за оптимизиране на производителността се създава локално копие на променливата currentLeg за всяка нишка, чиито промени не са видими за друга нишка. За да се реши този проблем, има ключовата дума volatile, която казва, че операция върху променлива, извършена в една нишка, трябва да бъде видима в други.
Сега две нишки, работещи в безкрайни цикли и опитващи се да прихванат взаимно споделения ресурс, решават нашия проблем.
Плащаневнимание на инструкцията Thread.yield(). Методът yield променя състоянието на нишката от Running на Ready и позволява на планировчика да превключи към друга нишка. По-конкретно, в нашия пример наличието или липсата на извикване на този метод няма да повлияе значително на резултата, но на практика може да направи превключването между нишките по-предсказуемо.
Но какво да кажем за ефективността на нашето решение? Нашето приложение ражда две нишки, които правят изчисления без спиране. Ако отворите системния монитор в операционната система или стартирате VisualVM, можете да забележите огромната консумация на CPU ресурси от нашата програма. Докато едната нишка произвежда изход към системата, другата се зацикля в нищото. Колкото по-дълго една нишка изпълнява полезния товар, толкова повече празна работа върши втората:
Отчитания на VisualVM |
Показания на системния монитор |
Вариант 2: Споделен монитор
Би било чудесно да спрете една нишка, докато друга работи, а след това да се събудите и да спрете втората нишка.
Класът Thread има методи suspend() и resume(), но те са остарели и се считат за опасни за използване.
За да отговорим на въпроса защо, нека си представим, че в програма, работеща в нишка, има работа с критични ресурси (например System.out), до които трябва да имаме достъп само през монитора:
Алтернатива на методите suspend() и resume() са методите wait(), notify() и notifyAll().
Методът wait() поставя нишката в състояние Waiting (или Timed Waiting, ако е указано време за изчакване), а методите notify() и notifyAll() я връщат в състояние Runnable.
Важно е да се разберече това са методи не от класа Thread, а от класа Object, които могат лесно да се споделят между нишки, което избягва горните трудности с методите suspend и resume.
Сега можем да споделяме общ монитор между нашите нишки и да предприемем стъпка, за да събудим всичките му собственици, след което можем спокойно да започнем да чакаме, докато някой ни събуди:
Това избягва блокировките, когато и двете нишки изчакват на един и същи монитор. По-конкретно, в нашия пример е много вероятно двете нишки едновременно да извикат метода за уведомяване и след това да заспят заедно и няма да има кой да ги събуди.
Важно е да не забравяте за това, защото, за съжаление, компилаторът не може да ви напомни за това и запомнянето на това по време на изпълнение е много неприятно!
От друга страна, методът на изчакване се изпълнява вътре в блока за синхронизиране и може да изглежда, че нишката никога няма да освободи монитора отново. Но всъщност не е така. Работата е там, че методът на изчакване освобождава монитора и когато нишката се събуди, тя влиза в състояние на изчакване на монитора, който е освободил, преди да заспи.
След като спяща нишка освободи монитора, може да има много такива спящи нишки - оттук съществуването на метода notifyAll() и забележката, че методът notify() събуждаслучайниспящи нишки.
Сега решението ни изглежда много по-разумно, защото се отървахме от загубата на ресурси за празни цикли!
Отчитания на VisualVM |
Показания на системния монитор |
Потоците също имат кошмари
Нишката може също да се събуди, без да бъде уведомена, прекъсната или изтекла време, така нареченото фалшиво събуждане.
Сега, поради неволносъбуждайки се, могат да се случат непоправими неща! Нашият робот може да падне*:
- Лявата ръка хвана въжето и заспа
- Дясната ръка събуди лявата и хвана въжето
- Лявата ръка е будна
- Лявата ръка пусна въжето
- Дясната ръка внезапно се събуди!
- Дясната ръка пусна въжето
- колапс!
За да предотвратим това, документацията ни съветва да извикаме метода за изчакване в цикъл с изрична проверка, за да видим дали трябва да се събудим:
Как да спрем потока?
Много начинаещи да научат за нишки в java имат въпрос: как мога да спра нишка като цяло? Кратък отговор: няма начин.
Въпреки факта, че класът Thread има подходящ метод stop(), той е маркиран като остарял и като цяло осъден на най-тежката анатема!
Защо? Една от причините е, че дадена нишка може да бъде спряна, докато притежавате монитора и извършвате критични операции, оставяйки променливия обект достъпен отвън в непредсказуемо състояние.
Например, разгледайте пример с парични сметки и прехвърляне на средства между тях:
Методът join() е метод за изчакване нишката да завърши своята работа. Той поставя нишкатаon, на която е бил извикан, в състояние на изчакване, докато нишкатаon, на която е извикан този метод, приключи.
Ако се опитаме да спрем първата транзакция с помощта на метода stop() веднага след стартирането, тогава има възможност потокът да бъде прекъснат, след като средствата бъдат дебитирани от първата сметка, но преди да бъдат прехвърлени към втората, и ще получимнеприятна ситуация с изчезването на средства в системата, което ще доведе до изключение по време на втората транзакция.
Ами ако наистина трябва да направим транзакциите прекъсваеми и все още да спираме нишки? В този случай трябва да можем да разберем, че те се опитват да прекратят транзакцията и да предприемат мерки за връщане назад на вече извършените операции.
За да посочите на дадена нишка, че нейната работа трябва да бъде прекъсната, съществува методът interrupt(). Ако нишката е чакала за изпълнение на метод wait(), sleep() или join(), когато методът interrupt() е бил извикан, ще бъде хвърлено InterruptedException. В този случай, ако нишката е извършвала изчисления (например работеща в цикъл), тогава тези изчисления няма да бъдат прекъснати и нишката просто ще бъде маркирана като прекъсната.