Java Thread Deadlock en Livelock

1. Overzicht

Hoewel multi-threading helpt bij het verbeteren van de prestaties van een applicatie, brengt het ook enkele problemen met zich mee. In deze tutorial kijken we naar twee van dergelijke problemen, deadlock en livelock, met behulp van Java-voorbeelden.

2. Deadlock

2.1. Wat is een impasse?

Er treedt een impasse op wanneer twee of meer threads wachten eeuwig op een vergrendeling of bron die wordt vastgehouden door een van de andere threads. Bijgevolg kan een toepassing vastlopen of mislukken omdat de vastgelopen threads niet verder kunnen.

Het probleem van de klassieke eetfilosofen illustreert mooi de synchronisatieproblemen in een multi-threaded omgeving en wordt vaak gebruikt als een voorbeeld van een impasse.

2.2. Deadlock Voorbeeld

Laten we eerst eens kijken naar een eenvoudig Java-voorbeeld om impasse te begrijpen.

In dit voorbeeld maken we twee threads, T1 en T2. Draad T1 oproepen operatie 1, en draad T2 oproepen operaties.

Om hun bewerkingen te voltooien, draad T1 moet verwerven slot 1 eerst en dan slot2, terwijl thread T2 moet verwerven slot2 eerst en dan slot 1. Dus in feite proberen beide threads de sloten in de tegenovergestelde volgorde te verwerven.

Laten we nu het Deadlock Voorbeeld klasse:

public class DeadlockExample {private Lock lock1 = new ReentrantLock (true); private Lock lock2 = nieuwe ReentrantLock (true); public static void main (String [] args) {DeadlockExample deadlock = new DeadlockExample (); nieuwe thread (deadlock :: operation1, "T1"). start (); nieuwe thread (deadlock :: operation2, "T2"). start (); } openbare ongeldige operatie1 () {lock1.lock (); print ("lock1 verworven, wachtend om lock2 te verwerven."); slaap (50); lock2.lock (); print ("lock2 verworven"); print ("voert eerste bewerking uit."); lock2.unlock (); lock1.unlock (); } openbare ongeldige operatie2 () {lock2.lock (); print ("lock2 verworven, wachtend om lock1 te verwerven."); slaap (50); lock1.lock (); print ("lock1 verworven"); print ("voert tweede bewerking uit."); lock1.unlock (); lock2.unlock (); } // hulpmethoden}

Laten we nu dit impasse-voorbeeld uitvoeren en de uitvoer opmerken:

Thread T1: lock1 verworven, wachtend om lock2 te verwerven. Thread T2: lock2 verworven, wachtend om lock1 te verwerven.

Zodra we het programma hebben uitgevoerd, kunnen we zien dat het programma een impasse veroorzaakt en nooit wordt afgesloten. Het logboek toont die draad T1 wacht op slot2, die wordt vastgehouden door draad T2. Evenzo thread T2 wacht op slot 1, die wordt vastgehouden door draad T1.

2.3. Deadlock vermijden

Deadlock is een veelvoorkomend probleem met gelijktijdigheid in Java. Daarom moeten we een Java-applicatie ontwerpen om mogelijke impasses te vermijden.

Om te beginnen moeten we de noodzaak vermijden om meerdere sloten voor een draad te kopen. Als een draad echter meerdere vergrendelingen nodig heeft, moeten we ervoor zorgen dat elke draad de vergrendelingen in dezelfde volgorde krijgt, om vermijd elke cyclische afhankelijkheid bij het verwerven van vergrendelingen.

We kunnen ook gebruik maken van getimede vergrendelingspogingen, zoals de tryLock methode in de Slot interface, om ervoor te zorgen dat een thread niet oneindig blokkeert als deze geen vergrendeling kan krijgen.

3. Livelock

3.1. Wat is Livelock

Livelock is een ander gelijktijdigheidsprobleem en lijkt op een impasse. In livelock, twee of meer threads blijven toestanden tussen elkaar overdragen in plaats van oneindig te wachten, zoals we zagen in het impasse-voorbeeld. Bijgevolg zijn de threads niet in staat hun respectieve taken uit te voeren.

Een goed voorbeeld van livelock is een berichtensysteem waarbij, wanneer zich een uitzondering voordoet, de berichtconsument de transactie terugdraait en het bericht terugzet naar de kop van de wachtrij. Vervolgens wordt hetzelfde bericht herhaaldelijk uit de wachtrij gelezen, alleen om weer een uitzondering te veroorzaken en weer in de wachtrij te worden geplaatst. De consument pikt nooit een ander bericht uit de wachtrij op.

3.2. Livelock Voorbeeld

Om de livelock-toestand te demonstreren, nemen we hetzelfde impasse-voorbeeld dat we eerder hebben besproken. In dit voorbeeld ook thread T1 oproepen operatie 1 en draad T2 oproepen operatie 2. We zullen de logica van deze bewerkingen echter enigszins wijzigen.

Beide draden hebben twee sloten nodig om hun werk te voltooien. Elke draad krijgt zijn eerste vergrendeling, maar vindt dat de tweede vergrendeling niet beschikbaar is. Dus om de andere draad als eerste te laten voltooien, geeft elke draad zijn eerste vergrendeling vrij en probeert hij beide vergrendelingen opnieuw te verwerven.

Laten we livelock demonstreren met een Livelock Voorbeeld klasse:

openbare klasse LivelockExample {private Lock lock1 = new ReentrantLock (true); private Lock lock2 = nieuwe ReentrantLock (true); public static void main (String [] args) {LivelockExample livelock = new LivelockExample (); nieuwe draad (livelock :: operation1, "T1"). start (); nieuwe draad (livelock :: operation2, "T2"). start (); } public void operation1 () {while (true) {tryLock (lock1, 50); print ("lock1 verworven, probeert lock2 te verkrijgen."); slaap (50); if (tryLock (lock2)) {print ("lock2 verworven."); } else {print ("kan lock2 niet verkrijgen, lock1 wordt vrijgegeven."); lock1.unlock (); doorgaan met; } print ("voert eerste bewerking uit."); breken; } lock2.unlock (); lock1.unlock (); } public void operation2 () {while (true) {tryLock (lock2, 50); print ("lock2 verworven, probeert lock1 te verkrijgen."); slaap (50); if (tryLock (lock1)) {print ("lock1 verworven."); } else {print ("kan lock1 niet verkrijgen, lock2 wordt vrijgegeven."); lock2.unlock (); doorgaan met; } print ("voert tweede bewerking uit."); breken; } lock1.unlock (); lock2.unlock (); } // helpermethoden}

Laten we nu dit voorbeeld uitvoeren:

Thread T1: lock1 verworven, in een poging om lock2 te verwerven. Thread T2: lock2 verworven, probeert lock1 te verwerven. Thread T1: kan lock2 niet verkrijgen, lock1 wordt vrijgegeven. Thread T2: kan lock1 niet verkrijgen, lock2 wordt vrijgegeven. Thread T2: lock2 verworven, probeert lock1 te verwerven. Thread T1: lock1 verworven, in een poging om lock2 te verwerven. Thread T1: kan lock2 niet verkrijgen, lock1 wordt vrijgegeven. Thread T1: lock1 verworven, in een poging om lock2 te verwerven. Thread T2: kan lock1 niet verkrijgen, lock2 wordt vrijgegeven. ..

Zoals we in de logboeken kunnen zien, nemen beide threads herhaaldelijk vergrendelingen op en geven ze weer vrij. Hierdoor kan geen van de threads de bewerking voltooien.

3.3. Livelock vermijden

Om een ‚Äč‚Äčlivelock te voorkomen, moeten we kijken naar de toestand die de livelock veroorzaakt en vervolgens een passende oplossing bedenken.

Als we bijvoorbeeld twee threads hebben die herhaaldelijk vergrendelingen verwerven en vrijgeven, wat resulteert in livelock, kunnen we de code zo ontwerpen dat de threads opnieuw proberen om de vergrendelingen met willekeurige tussenpozen te verkrijgen. Dit geeft de draden een eerlijke kans om de sloten te bemachtigen die ze nodig hebben.

Een andere manier om het levensprobleem op te lossen in het voorbeeld van het berichtensysteem dat we eerder hebben besproken, is door mislukte berichten in een aparte wachtrij te plaatsen voor verdere verwerking in plaats van ze opnieuw in dezelfde wachtrij te plaatsen.

4. Conclusie

In deze tutorial hebben we deadlock en livelock besproken. We hebben ook gekeken naar Java-voorbeelden om elk van deze problemen te demonstreren en hebben kort besproken hoe we ze kunnen vermijden.

Zoals altijd is de volledige code die in dit voorbeeld wordt gebruikt, te vinden op GitHub.