1. Общ преглед
Докато многопоточността помага за подобряване на производителността на дадено приложение, то идва и с някои проблеми. В този урок ще разгледаме два такива проблема, задръстване и ливелок, с помощта на примери за Java.
2. Тупик
2.1. Какво е задънена улица?
Блокиране възниква, когато две или повече нишки чакат вечно за заключване или ресурс, държани от друга от нишките . Следователно приложението може да спре или да се провали, тъй като блокиращите нишки не могат да прогресират.
Класическият проблем на философите за хранене демонстрира добре проблемите със синхронизирането в среда с много нишки и често се използва като пример за блокиране.
2.2. Пример за задънена улица
Първо, нека разгледаме един прост пример за Java, за да разберем блокирането.
В този пример ще създадем две нишки, T1 и T2 . Thread T1 извиква операция1 и нишка T2 извиква операции .
За да завършат своите операции, нишката T1 трябва първо да придобие lock1 и след това lock2 , докато нишката T2 първо трябва да придобие lock2 и след това lock1 . И така, и двете нишки се опитват да придобият ключалките в обратен ред.
Сега, нека напишем класа DeadlockExample :
public class DeadlockExample { private Lock lock1 = new ReentrantLock(true); private Lock lock2 = new ReentrantLock(true); public static void main(String[] args) { DeadlockExample deadlock = new DeadlockExample(); new Thread(deadlock::operation1, "T1").start(); new Thread(deadlock::operation2, "T2").start(); } public void operation1() { lock1.lock(); print("lock1 acquired, waiting to acquire lock2."); sleep(50); lock2.lock(); print("lock2 acquired"); print("executing first operation."); lock2.unlock(); lock1.unlock(); } public void operation2() { lock2.lock(); print("lock2 acquired, waiting to acquire lock1."); sleep(50); lock1.lock(); print("lock1 acquired"); print("executing second operation."); lock1.unlock(); lock2.unlock(); } // helper methods }
Нека сега стартираме този пример за блокиране и забележим резултата:
Thread T1: lock1 acquired, waiting to acquire lock2. Thread T2: lock2 acquired, waiting to acquire lock1.
След като стартираме програмата, можем да видим, че програмата води до задънена улица и никога не излиза. Дневникът показва, че нишката T1 чака lock2 , която се задържа от нишка T2 . По подобен начин нишка T2 чака lock1 , която се държи от нишка T1 .
2.3. Избягване на задънена улица
Deadlock е често срещан проблем за едновременност в Java. Следователно трябва да проектираме Java приложение, за да избегнем всякакви потенциални условия на задънена улица.
Като начало трябва да избегнем необходимостта от придобиване на множество ключалки за нишка. Ако обаче нишката се нуждае от множество ключалки, трябва да се уверим, че всяка нишка придобива ключалките в същия ред, за да се избегне всякаква циклична зависимост при придобиването на заключване .
Можем да използваме и опити за заключване по време , като метода tryLock в интерфейса за заключване , за да сме сигурни, че нишката няма да блокира безкрайно, ако не е в състояние да получи заключване.
3. Livelock
3.1. Какво е Livelock
Livelock е друг проблем за съвпадение и е подобен на блокирането. В livelock две или повече нишки продължават да прехвърлят състояния помежду си, вместо да чакат безкрайно, както видяхме в примера за задънена улица. Следователно нишките не могат да изпълняват съответните си задачи.
Чудесен пример за livelock е система за съобщения, при която при възникване на изключение потребителят на съобщението връща транзакцията и връща съобщението обратно в главата на опашката. Тогава едно и също съобщение се чете многократно от опашката, само за да предизвика ново изключение и да бъде върнато на опашката. Потребителят никога няма да вземе друго съобщение от опашката.
3.2. Пример за Livelock
Сега, за да демонстрираме състоянието на livelock, ще вземем същия пример за блокиране, който обсъждахме по-рано. В този пример също нишка T1 извиква операция1 и нишка Т2 извиква операция2 . Ние обаче ще променим леко логиката на тези операции.
И двете нишки се нуждаят от две ключалки, за да завършат работата си. Всяка нишка придобива първата си ключалка, но установява, че втората ключалка не е налична. И така, за да може другата нишка да завърши първо, всяка нишка освобождава първата си ключалка и се опитва да придобие и двете ключалки отново.
Нека демонстрираме livelock с клас LivelockExample :
public class LivelockExample { private Lock lock1 = new ReentrantLock(true); private Lock lock2 = new ReentrantLock(true); public static void main(String[] args) { LivelockExample livelock = new LivelockExample(); new Thread(livelock::operation1, "T1").start(); new Thread(livelock::operation2, "T2").start(); } public void operation1() { while (true) { tryLock(lock1, 50); print("lock1 acquired, trying to acquire lock2."); sleep(50); if (tryLock(lock2)) { print("lock2 acquired."); } else { print("cannot acquire lock2, releasing lock1."); lock1.unlock(); continue; } print("executing first operation."); break; } lock2.unlock(); lock1.unlock(); } public void operation2() { while (true) { tryLock(lock2, 50); print("lock2 acquired, trying to acquire lock1."); sleep(50); if (tryLock(lock1)) { print("lock1 acquired."); } else { print("cannot acquire lock1, releasing lock2."); lock2.unlock(); continue; } print("executing second operation."); break; } lock1.unlock(); lock2.unlock(); } // helper methods }
Сега нека пуснем този пример:
Thread T1: lock1 acquired, trying to acquire lock2. Thread T2: lock2 acquired, trying to acquire lock1. Thread T1: cannot acquire lock2, releasing lock1. Thread T2: cannot acquire lock1, releasing lock2. Thread T2: lock2 acquired, trying to acquire lock1. Thread T1: lock1 acquired, trying to acquire lock2. Thread T1: cannot acquire lock2, releasing lock1. Thread T1: lock1 acquired, trying to acquire lock2. Thread T2: cannot acquire lock1, releasing lock2. ..
Както виждаме в дневниците, и двете нишки многократно придобиват и освобождават ключалки. Поради това никоя от нишките не е в състояние да завърши операцията.
3.3. Избягване на Livelock
За да избегнем ливокла, трябва да проучим състоянието, което причинява ливоклата, и след това да намерим решение по съответния начин.
Например, ако имаме две нишки, които многократно придобиват и освобождават брави, което води до livelock, можем да проектираме кода така, че нишките да опитат да придобият ключалките на произволни интервали. Това ще даде на нишките справедлив шанс да придобият необходимите им ключалки.
Друг начин да се погрижим за проблема с оживяването в примера за система за съобщения, който обсъдихме по-рано, е да поставим неуспешните съобщения в отделна опашка за по-нататъшна обработка, вместо да ги върнем отново в същата опашка.
4. Заключение
В този урок ние обсъдихме мъртва и безразсъдна ситуация. Освен това разгледахме примери за Java, за да демонстрираме всеки от тези проблеми и накратко засегнахме как можем да ги избегнем.
Както винаги, пълният код, използван в този пример, може да бъде намерен в GitHub.