JavaRush /Java-Blog /Random-DE /Deadlock in Java und Methoden zu seiner Bekämpfung
articles
Level 15

Deadlock in Java und Methoden zu seiner Bekämpfung

Veröffentlicht in der Gruppe Random-DE
Bei der Entwicklung von Multithread-Anwendungen entsteht häufig ein Dilemma: Wichtiger ist die Zuverlässigkeit oder Leistung der Anwendung. Beispielsweise verwenden wir die Synchronisierung zur Thread-Sicherheit. In Fällen, in denen die Synchronisierungsreihenfolge falsch ist, kann es zu Deadlocks kommen. Wir verwenden außerdem Thread-Pools und Semaphoren, um den Ressourcenverbrauch zu begrenzen. Ein Fehler in diesem Design kann aufgrund fehlender Ressourcen zu einem Deadlock führen. In diesem Artikel werden wir darüber sprechen, wie man Deadlocks und andere Probleme bei der Leistung der Anwendung vermeidet. Wir werden auch untersuchen, wie eine Anwendung so geschrieben werden kann, dass sie im Falle eines Deadlocks wiederhergestellt werden kann. Deadlock in Java und Methoden zu seiner Bekämpfung - 1Deadlock ist eine Situation, in der zwei oder mehr Prozesse, die einige Ressourcen belegen, versuchen, andere Ressourcen zu erhalten, die von anderen Prozessen belegt sind, und keiner der Prozesse die benötigte Ressource belegen und dementsprechend die belegte freigeben kann. Diese Definition ist zu allgemein und daher schwer verständlich; zum besseren Verständnis betrachten wir die Arten von Deadlocks anhand von Beispielen.

Gegenseitige Sperrung der Synchronisationsreihenfolge

Betrachten Sie die folgende Aufgabe: Sie müssen eine Methode schreiben, die eine Transaktion ausführt, um einen bestimmten Geldbetrag von einem Konto auf ein anderes zu überweisen. Die Lösung könnte so aussehen:
public void transferMoney(Account fromAccount, Account toAccount, Amount amount) throws InsufficientFundsException {
	synchronized (fromAccount) {
		synchronized (toAccount) {
			if (fromAccount.getBalance().compareTo(amount) < 0)
				throw new InsufficientFundsException();
			else {
				fromAccount.debit(amount);
				toAccount.credit(amount);
			}
		}
	}
}
Auf den ersten Blick ist dieser Code ganz normal synchronisiert; wir haben eine atomare Operation zum Überprüfen und Ändern des Status des Quellkontos und zum Ändern des Zielkontos. Bei dieser Synchronisationsstrategie kann es jedoch zu einem Deadlock kommen. Schauen wir uns ein Beispiel an, wie dies geschieht. Es müssen zwei Transaktionen durchgeführt werden: x Geld von Konto A auf Konto B überweisen und y Geld von Konto B auf Konto A überweisen. Oft führt diese Situation nicht zu einem Deadlock. Unter unglücklichen Umständen belegt jedoch Transaktion 1 den Kontomonitor A und Transaktion 2 den Kontomonitor B. Das Ergebnis ist ein Deadlock: Transaktion 1 wartet darauf, dass Transaktion 2 den Kontomonitor freigibt B, aber Transaktion 2 muss auf Monitor A zugreifen, der von Transaktion 1 belegt ist. Eines der großen Probleme bei Deadlocks besteht darin, dass sie beim Testen nicht leicht zu finden sind. Selbst in der im Beispiel beschriebenen Situation dürfen Threads nicht blockieren, d. h. diese Situation wird nicht ständig reproduziert, was die Diagnose erheblich erschwert. Im Allgemeinen ist das beschriebene Problem des Nichtdeterminismus typisch für Multithreading (obwohl es dadurch nicht einfacher wird). Daher spielt die Codeüberprüfung eine wichtige Rolle bei der Verbesserung der Qualität von Multithread-Anwendungen, da sie es Ihnen ermöglicht, Fehler zu identifizieren, die beim Testen schwer zu reproduzieren sind. Das bedeutet natürlich nicht, dass die Anwendung nicht getestet werden muss; wir sollten nur die Codeüberprüfung nicht vergessen. Was muss ich tun, um zu verhindern, dass dieser Code einen Deadlock verursacht? Diese Blockierung wird dadurch verursacht, dass die Kontosynchronisierung in einer anderen Reihenfolge erfolgen kann. Wenn Sie dementsprechend eine Ordnung auf den Konten einführen (dies ist eine Regel, die es Ihnen ermöglicht zu sagen, dass Konto A kleiner ist als Konto B), wird das Problem behoben. Wie kann man das machen? Erstens: Wenn Konten eine Art eindeutige Kennung haben (z. B. eine Kontonummer), numerisch, in Kleinbuchstaben oder auf andere Weise mit einem natürlichen Ordnungskonzept (Zeichenfolgen können in lexikografischer Reihenfolge verglichen werden), können wir uns glücklich schätzen, und das werden wir auch Immer Wir können zuerst den Monitor des kleineren Kontos belegen und dann den größeren (oder umgekehrt).
private void doTransfer(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	if (fromAcct.getBalance().compareTo(amount) < 0)
		throw new InsufficientFundsException();
	else {
		fromAcct.debit(amount);
		toAcct.credit(amount);
	}
}
public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	int fromId= fromAcct.getId();
	int toId = fromAcct.getId();
	if (fromId < toId) {
		synchronized (fromAcct) {
			synchronized (toAcct) {
				doTransfer(fromAcct, toAcct, amount)}
			}
		}
	} else  {
		synchronized (toAcct) {
			synchronized (fromAcct) {
				doTransfer(fromAcct, toAcct, amount)}
			}
		}
	}
}
Die zweite Option: Wenn wir keinen solchen Identifikator haben, müssen wir ihn uns selbst ausdenken. In erster Näherung können wir Objekte anhand des Hash-Codes vergleichen. Höchstwahrscheinlich werden sie unterschiedlich sein. Was aber, wenn sich herausstellt, dass sie gleich sind? Anschließend müssen Sie ein weiteres Objekt zur Synchronisierung hinzufügen. Es sieht vielleicht etwas anspruchsvoll aus, aber was kann man tun? Und außerdem wird das dritte Objekt recht selten verwendet. Das Ergebnis wird so aussehen:
private static final Object tieLock = new Object();
private void doTransfer(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	if (fromAcct.getBalance().compareTo(amount) < 0)
		throw new InsufficientFundsException();
	else {
		fromAcct.debit(amount);
		toAcct.credit(amount);
	}
}
public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	int fromHash = System.identityHashCode(fromAcct);
	int toHash = System.identityHashCode(toAcct);
	if (fromHash < toHash) {
		synchronized (fromAcct) {
			synchronized (toAcct) {
				doTransfer(fromAcct, toAcct, amount);
			}
		}
	} else if (fromHash > toHash) {
		synchronized (toAcct) {
			synchronized (fromAcct) {
				doTransfer(fromAcct, toAcct, amount);
			}
		}
	} else {
		synchronized (tieLock) {
			synchronized (fromAcct) {
				synchronized (toAcct) {
					doTransfer(fromAcct, toAcct, amount)
				}
			}
		}
	}
}

Deadlock zwischen Objekten

Die beschriebenen Blockierbedingungen stellen den am einfachsten zu diagnostizierenden Fall eines Deadlocks dar. In Multithread-Anwendungen versuchen häufig verschiedene Objekte, auf dieselben synchronisierten Blöcke zuzugreifen. Dies kann zu einem Deadlock führen. Betrachten Sie das folgende Beispiel: eine Flug-Dispatcher-Anwendung. Flugzeuge melden dem Fluglotsen, wenn sie an ihrem Ziel angekommen sind, und bitten um Landeerlaubnis. Der Lotse speichert alle Informationen über in seine Richtung fliegende Flugzeuge und kann deren Position auf der Karte eintragen.
class Plane {
	private Point location, destination;
	private final Dispatcher dispatcher;

	public Plane(Dispatcher dispatcher) {
		this.dispatcher = dispatcher;
	}
	public synchronized Point getLocation() {
		return location;
	}
	public synchronized void setLocation(Point location) {
		this.location = location;
		if (location.equals(destination))
		dispatcher.requestLanding(this);
	}
}

class Dispatcher {
	private final Set<Plane> planes;
	private final Set<Plane> planesPendingLanding;

	public Dispatcher() {
		planes = new HashSet<Plane>();
		planesPendingLanding = new HashSet<Plane>();
	}
	public synchronized void requestLanding(Plane plane) {
		planesPendingLanding.add(plane);
	}
	public synchronized Image getMap() {
		Image image = new Image();
		for (Plane plane : planes)
			image.drawMarker(plane.getLocation());
		return image;
	}
}
Es ist schwieriger zu verstehen, dass dieser Code einen Fehler enthält, der zu einem Deadlock führen kann, als im vorherigen. Auf den ersten Blick gibt es keine Neusynchronisierungen, aber das ist nicht der Fall. Sie haben wahrscheinlich bereits bemerkt, dass setLocationKlasse Planeund getMapKlassenmethoden Dispatchersynchronisiert sind und synchronisierte Methoden anderer Klassen in sich selbst aufrufen. Dies ist im Allgemeinen eine schlechte Praxis. Wie dies korrigiert werden kann, wird im nächsten Abschnitt besprochen. Wenn das Flugzeug am Standort ankommt, kann es daher in dem Moment, in dem jemand beschließt, die Karte zu erhalten, zu einem Stillstand kommen. Das heißt, es werden die Methoden getMapund aufgerufen, setLocationdie jeweils die Instanzmonitore Dispatcherbzw. belegen Plane. Die Methode ruft dann getMapauf plane.getLocation(insbesondere die Instanz Plane, die gerade beschäftigt ist), die darauf wartet, dass der Monitor für jede der Instanzen frei wird Plane. Gleichzeitig setLocationwird die Methode aufgerufen dispatcher.requestLanding, während der Instanzmonitor Dispatcherweiterhin mit dem Zeichnen der Karte beschäftigt ist. Das Ergebnis ist ein Deadlock.

Offene Anrufe

Um Situationen wie die im vorherigen Abschnitt beschriebene zu vermeiden, empfiehlt es sich, öffentliche Aufrufe von Methoden anderer Objekte zu verwenden. Rufen Sie also Methoden anderer Objekte außerhalb des synchronisierten Blocks auf. Wenn Methoden nach dem Prinzip offener Aufrufe umgeschrieben werden setLocation, getMapwird die Möglichkeit eines Deadlocks eliminiert. Es wird zum Beispiel so aussehen:
public void setLocation(Point location) {
	boolean reachedDestination;
	synchronized(this){
		this.location = location;
		reachedDestination = location.equals(destination);
	}
	if (reachedDestination)
		dispatcher.requestLanding(this);
}
………………………………………………………………………………
public Image getMap() {
	Set<Plane> copy;
	synchronized(this){
		copy = new HashSet<Plane>( planes);
	}
	Image image = new Image();
	for (Plane plane : copy)
		image.drawMarker(plane.getLocation());
	return image;
}

Ressourcen-Deadlock

Deadlocks können auch auftreten, wenn versucht wird, auf einige Ressourcen zuzugreifen, die jeweils nur ein Thread verwenden kann. Ein Beispiel wäre ein Datenbankverbindungspool. Wenn einige Threads gleichzeitig auf zwei Verbindungen zugreifen müssen und in unterschiedlicher Reihenfolge darauf zugreifen, kann dies zu einem Deadlock führen. Grundsätzlich unterscheidet sich diese Art der Sperrung nicht von der Sperrung der Synchronisationsreihenfolge, mit der Ausnahme, dass sie nicht beim Versuch auftritt, Code auszuführen, sondern beim Versuch, auf Ressourcen zuzugreifen.

Wie vermeide ich Deadlocks?

Wenn der Code fehlerfrei geschrieben ist (Beispiele dafür haben wir in den vorherigen Abschnitten gesehen), gibt es natürlich keine Deadlocks. Aber wer kann garantieren, dass sein Code fehlerfrei geschrieben ist? Natürlich hilft das Testen, einen erheblichen Teil der Fehler zu identifizieren, aber wie wir bereits gesehen haben, sind Fehler in Multithread-Code nicht einfach zu diagnostizieren und selbst nach dem Testen kann man nicht sicher sein, dass es keine Deadlock-Situationen gibt. Können wir uns irgendwie vor Blockaden schützen? Die Antwort ist ja. Ähnliche Techniken werden in Datenbank-Engines verwendet, die häufig Deadlocks (im Zusammenhang mit dem Transaktionsmechanismus in der Datenbank) beheben müssen. Die Lockim Paket verfügbare Schnittstelle und ihre Implementierungen java.util.concurrent.locksermöglichen es Ihnen, mithilfe der Methode zu versuchen, den einer Instanz dieser Klasse zugeordneten Monitor zu belegen tryLock(gibt true zurück, wenn der Monitor belegt werden konnte). Angenommen, wir haben ein Objektpaar, das eine Schnittstelle implementiert Lock, und wir müssen ihre Monitore so belegen, dass eine gegenseitige Blockierung vermieden wird. Sie können es so umsetzen:
public void twoLocks(Lock A,  Lock B){
	while(true){
		if(A.tryLock()){
			if(B.tryLock())
			{
				try{
					//do something
				} finally{
					B.unlock();
					A.unlock();
				}
			} else{
				A.unlock();
			}
		}
	}
}
Wie Sie in diesem Programm sehen können, belegen wir zwei Monitore und eliminieren gleichzeitig die Möglichkeit einer gegenseitigen Blockierung. Bitte beachten Sie, dass die Blockierung try- finallyerforderlich ist, da die Klassen im Paket java.util.concurrent.locksden Monitor nicht automatisch freigeben. Wenn während der Ausführung Ihrer Aufgabe eine Ausnahme auftritt, bleibt der Monitor in einem gesperrten Zustand hängen. Wie erkennt man Deadlocks? Mit der JVM können Sie Deadlocks diagnostizieren, indem Sie sie in Thread-Dumps anzeigen. Solche Dumps enthalten Informationen darüber, in welchem ​​Zustand sich der Thread befindet. Wenn er blockiert ist, enthält der Dump Informationen über den Monitor, auf dessen Freigabe der Thread wartet. Vor dem Dumping von Threads schaut sich die JVM das Diagramm der wartenden (beschäftigten) Monitore an. Wenn sie Zyklen findet, fügt sie Deadlock-Informationen hinzu, die die teilnehmenden Monitore und Threads angeben. Ein Dump blockierter Threads sieht so aus:
Found one Java-level deadlock:
=============================
"ApplicationServerThread":
waiting to lock monitor 0x0f0d80cc (a MyDBConnection),
which is held by "ApplicationServerThread"
"ApplicationServerThread":
waiting to lock monitor 0x0f0d8fed (a MyDBCallableStatement),
which is held by "ApplicationServerThread"
Java stack information for the threads listed above:
"ApplicationServerThread":
at MyDBConnection.remove_statement
- waiting to lock <0x6f50f730> (a MyDBConnection)
at MyDBStatement.close
- locked <0x604ffbb0> (a MyDBCallableStatement)
...
"ApplicationServerThread":
at MyDBCallableStatement.sendBatch
- waiting to lock <0x604ffbb0> (a MyDBCallableStatement)
at MyDBConnection.commit
- locked <0x6f50f730> (a MyDBConnection)
Der obige Dump zeigt deutlich, dass sich zwei Threads, die mit der Datenbank arbeiten, gegenseitig blockiert haben. Um Deadlocks mithilfe dieser JVM-Funktion zu diagnostizieren, ist es notwendig, an verschiedenen Stellen im Programm Aufrufe für den Thread-Dump-Vorgang zu platzieren und die Anwendung zu testen. Als nächstes sollten Sie die resultierenden Protokolle analysieren. Wenn sie darauf hinweisen, dass ein Deadlock aufgetreten ist, helfen die Informationen aus dem Dump dabei, die Bedingungen zu erkennen, unter denen dieser aufgetreten ist. Generell sollten Sie Situationen wie in den Deadlock-Beispielen vermeiden. In solchen Fällen wird die Anwendung höchstwahrscheinlich stabil funktionieren. Aber vergessen Sie nicht das Testen und die Codeüberprüfung. Dies hilft bei der Identifizierung von Problemen, falls diese auftreten. In Fällen, in denen Sie ein System entwickeln, für das die Wiederherstellung des Deadlock-Felds von entscheidender Bedeutung ist, können Sie die im Abschnitt „Wie vermeidet man Deadlocks?“ beschriebene Methode verwenden. In diesem Fall ist die lockInterruptiblySchnittstellenmethode Lockaus der java.util.concurrent.locks. Mit dieser Methode können Sie den Thread unterbrechen, der den Monitor belegt hat (und so den Monitor freigeben).
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION