Programmieren von Java-Threads in der realen Welt, Teil 2

In seinem kürzlich erschienenen Artikel "Design for Thread Safety" (Teil der Spalte " Design Techniques" ) stellte Bill Venners den Begriff einer Race-Bedingung vor, bei der zwei Threads gleichzeitig um dasselbe Objekt kämpfen und das Objekt infolgedessen belassen ein undefinierter Zustand. Bill wies darauf hin, dass JavasynchronizedDas Schlüsselwort ist in der Sprache, um diese Probleme zu vermeiden, und er lieferte ein einfaches Beispiel für seine Verwendung. Der Artikel vom letzten Monat war der erste in einer Reihe über Themen. Der vorliegende Artikel setzt diese Serie fort und behandelt die Rennbedingungen ausführlich. Außerdem werden verschiedene Deadlock-Szenarien erörtert, die sich auf die Rennbedingungen beziehen, jedoch viel schwieriger zu finden und zu debuggen sind. In diesem Monat konzentrieren wir uns hauptsächlich auf die Probleme beim Programmieren von Java-Threads. Nachfolgende Java Toolbox- Spalten konzentrieren sich ausschließlich auf Lösungen.

Monitore, Mutexe und Badezimmer

Um sicherzustellen, dass wir alle am selben Ort beginnen, ist eine kleine Überprüfung der Begriffe angebracht. Das zentrale Konzept für die Synchronisation im Java-Modell ist der Monitor, der vor etwa 20 Jahren von CAR Hoare entwickelt wurde. Ein Monitor ist ein Code, der durch ein Semaphor zum gegenseitigen Ausschluss (oder, um einen bei Digital Equipment Corp. geprägten Begriff zu verwenden, einen Mutex ) geschützt wird). Der zentrale Begriff eines Mutex betrifft das Eigentum. Es kann immer nur ein Thread den Mutex besitzen. Wenn ein zweiter Thread versucht, das Eigentum zu "erwerben", wird er blockiert (ausgesetzt), bis der besitzende Thread den Mutex "freigibt". Wenn mehrere Threads darauf warten, denselben Mutex zu erhalten, werden sie alle gleichzeitig freigegeben, wenn der besitzende Thread den Mutex freigibt. Die freigegebenen Threads müssen dann untereinander aussortieren, wer das Eigentum erlangt. (In der Regel wird die Prioritätsreihenfolge, die FIFO-Reihenfolge oder eine Kombination davon verwendet, um zu bestimmen, welcher Thread die Kontrolle erhält.) Sie schützeneinen Codeblock, indem ein Mutex oben im Block erfasst und unten freigegeben wird. Der Code, aus dem der Monitor besteht, muss nicht zusammenhängend sein: Mehrere nicht zusammenhängende Codeblöcke können denselben Mutex erfassen und freigeben. In diesem Fall wird der gesamte Code als auf demselben Monitor befindlich betrachtet, da er einen gemeinsamen Mutex verwendet.

Die beste Analogie, die ich für einen Monitor gehört habe, ist ein Flugzeugbad. Es kann immer nur eine Person im Badezimmer sein (wir hoffen). Alle anderen stehen in einem ziemlich engen Gang in der Warteschlange und warten darauf, ihn zu benutzen. Solange die Tür verschlossen ist, ist das Badezimmer nicht zugänglich. Unter diesen Bedingungen ist in unserer Analogie das Objekt das Flugzeug, das Badezimmer der Monitor (vorausgesetzt, es gibt nur ein Badezimmer) und das Schloss an der Tür ist der Mutex.

In Java ist jedem Objekt nur ein Monitor und ein Mutex zugeordnet. Der einzelne Monitor verfügt jedoch über mehrere Türen, die jeweils durch das synchronizedSchlüsselwort gekennzeichnet sind. Wenn ein Thread über das synchronizedSchlüsselwort läuft , werden alle Türen effektiv verriegelt. Wenn ein Thread nicht über das synchronizedSchlüsselwort läuft , hat er die Tür natürlich nicht verriegelt, und ein anderer Thread ist jederzeit frei.

Beachten Sie, dass der Monitor dem Objekt und nicht der Klasse zugeordnet ist. Mehrere Threads können alle dieselbe Methode parallel ausführen, aber die empfangenden Objekte (wie durch die thisReferenzen gekennzeichnet) müssen unterschiedlich sein. Beispielsweise können mehrere Instanzen einer thread-sicheren Warteschlange von mehreren Threads verwendet werden. Diese Threads können Objekte gleichzeitig in verschiedene Warteschlangen einreihen, jedoch nicht gleichzeitig in dieselbe Warteschlange. Für eine bestimmte Warteschlange kann sich jeweils nur ein Thread im Monitor befinden.

Um die frühere Analogie zu verfeinern, ist das Flugzeug immer noch das Objekt, aber der Monitor besteht eigentlich aus allen Badezimmern zusammen (jeder synchronisierte Codeabschnitt ist ein Badezimmer). Wenn ein Faden in ein Badezimmer gelangt, sind die Türen aller Badezimmer verschlossen. Unterschiedliche Instanzen einer Klasse sind jedoch unterschiedliche Flugzeuge. Wenn die Badezimmertüren in Ihrem Flugzeug entriegelt sind, müssen Sie sich nicht wirklich um den Zustand der Türen in den anderen Flugzeugen kümmern.

Warum ist stop () in JDK 1.2 veraltet?

Die Tatsache, dass der Monitor in jedes Java-Objekt integriert ist, ist eigentlich etwas umstritten. Einige der Probleme, die mit dem Bündeln der Bedingungsvariablen und des Mutex in jedem Java-Objekt verbunden sind, wurden in JDK 1.2 durch die einfache Zweckmäßigkeit behoben, die problematischsten Methoden der ThreadKlasse zu verwerfen : stop()und suspend(). Sie können einen Thread blockieren, wenn Sie eine dieser Methoden innerhalb einer eigenen synchronisierten Methode aufrufen. Schauen Sie sich die folgende Methode an und denken Sie daran, dass der Mutex das Schloss an der Tür ist und der Faden, der die Tür verriegelt, den Monitor "besitzt". Aus diesem Grund sind die Methoden stop()und suspend()in JDK 1.2 veraltet. Betrachten Sie diese Methode:

Klasse some_class {// ... synchronisiert void f () {Thread.currentThread (). stop (); }}

Überlegen Sie nun, was passiert, wenn ein Thread aufruft f(). Der Thread erhält die Sperre beim Betreten des Monitors, stoppt dann aber. Der Mutex wird von nicht freigegebenstop() . Dies ist das Äquivalent zu jemandem, der ins Badezimmer geht, die Tür abschließt und sich in die Toilette spült! Jeder andere Thread, der jetzt versucht, f()dasselbe Objekt aufzurufen (oder eine andere synchronisierte Methode der Klasse), wird für immer blockiert. Die suspend()Methode (die ebenfalls veraltet ist) hat das gleiche Problem. Die sleep()Methode (die nicht veraltet ist) kann genauso schwierig sein. (Jemand geht ins Badezimmer, schließt die Tür ab und schläft ein). Denken Sie auch daran, dass Java-Objekte, auch solche, die erweitert Threadoder implementiert werdenRunnable, bestehen weiter, auch wenn der zugehörige Thread gestoppt wurde. Sie können tatsächlich eine synchronisierte Methode für ein Objekt aufrufen, das einem gestoppten Thread zugeordnet ist. Achtung.

Rennbedingungen und Schleusen

Eine Race-Bedingung tritt auf, wenn zwei Threads gleichzeitig versuchen, auf dasselbe Objekt zuzugreifen, und sich das Verhalten des Codes abhängig davon ändert, wer gewinnt. Das folgende Diagramm zeigt ein einzelnes (nicht synchronisiertes) Objekt, auf das mehrere Threads gleichzeitig zugreifen. Ein Thread kann fred()nach dem Ändern eines Felds, aber vor dem Ändern des anderen Felds vorbelegt werden. Wenn an diesem Punkt ein anderer Thread vorbeikommt und eine der gezeigten Methoden aufruft, bleibt das Objekt in einem instabilen Zustand, da der ursprüngliche Thread schließlich aufwacht und das andere Feld ändert.

Normalerweise denken Sie an Objekte, die Nachrichten an andere Objekte senden. In Multithread-Umgebungen müssen Sie an Nachrichtenhandler denken, die auf Threads ausgeführt werden. Denken Sie: Dieser Thread bewirkt, dass dieses Objekt eine Nachricht an dieses Objekt sendet. Eine Race-Bedingung kann auftreten, wenn zwei Threads dazu führen, dass Nachrichten gleichzeitig an dasselbe Objekt gesendet werden.

Arbeiten mit wait()undnotify()

Der folgende Code zeigt eine Blockierungswarteschlange, die für einen Thread verwendet wird, um einen anderen zu benachrichtigen, wenn ein Ereignis auftritt. (Wir werden in einem zukünftigen Artikel eine realistischere Version davon sehen, aber im Moment möchte ich die Dinge einfach halten.) Die Grundidee ist, dass ein Thread, der versucht, aus einer leeren Warteschlange aus der Warteschlange zu entfernen, blockiert, bis ein anderer Thread etwas einfügt in der Warteschlange.

Klasse Notifying_queue {private static final queue_size = 10; privates Objekt [] Warteschlange = neues Objekt [Warteschlangengröße]; private int head = 0; private int tail = 0; public void synchronized enqueue (Objektelement) {queue [++ head% = queue_size] = item; this.notify (); // Das "dies" dient nur dazu, die Lesbarkeit zu verbessern. public Object synchronized dequeue () {try {if (head == tail) // <=== Dies ist ein Fehler this.wait (); } catch (InterruptedException e) {// Wenn wir hierher kommen, wurden wir nicht benachrichtigt. // Die Rückgabe von null bedeutet nicht, dass die // Warteschlange leer ist, sondern nur, dass das Warten abgebrochen wurde. return null; } return queue [++ tail% = queue_size]; }}

Beginnen wir mit einer leeren Warteschlange und folgen wir der Abfolge der Operationen im Spiel, wenn ein Thread eine Warteschlange ausführt und ein anderer (zu einem späteren Zeitpunkt) ein Element in die Warteschlange stellt.

  1. Der Warteschlangen-Thread ruft auf dequeue(), betritt den Monitor (und sperrt andere Threads), bis der Monitor freigegeben wird. Der Thread testet auf eine leere Warteschlange ( head==tailwait (). (Ich werde gleich erklären, warum dies ein Fehler ist.)

  2. Das wait()gibt die Sperre frei. (Der aktuelle Thread verlässt vorübergehend den Monitor.) Anschließend wird ein zweites Synchronisationsobjekt blockiert, das als Bedingungsvariable bezeichnet wird. (Der Grundbegriff einer Bedingungsvariablen ist, dass ein Thread blockiert, bis eine Bedingung erfüllt ist. Im Fall der in Java integrierten Bedingungsvariablen wartet der Thread, bis die benachrichtigte Bedingung durch einen anderen Thread-Aufruf auf true gesetzt wird notify.) Es ist wichtig zu wissen, dass der wartende Thread den Monitor zu diesem Zeitpunkt freigegeben hat.

  3. Ein zweiter Thread kommt jetzt und stellt ein Objekt in notify()die Warteschlange , ruft schließlich auf und gibt dadurch den wartenden Thread (Warteschlange) frei.

  4. Der Warteschlangenthread muss nun warten, um den Monitor wait()erneut abzurufen, bevor er zurückkehren kann. Daher wird er erneut blockiert, diesmal für die dem Monitor zugeordnete Sperre.

  5. Der Thread in der Warteschlange kehrt von der enqueue()Methode zurück, die den Monitor freigibt.

  6. Der Warteschlangenthread erfasst den Monitor, wait()kehrt zurück, ein Objekt wird aus der Warteschlange entfernt und dequeue()kehrt zurück, wobei der Monitor freigegeben wird.

Rennbedingungen nach einer Wartezeit ()

Welche Probleme können nun auftreten? Die wichtigsten sind unerwartete Rennbedingungen. Was ist, wenn Sie den notify()Anruf durch einen Anruf bei ersetzt haben notifyAll()? Stellen Sie sich vor, mehrere Threads versuchten gleichzeitig, etwas aus derselben leeren Warteschlange zu entfernen. Alle diese Threads werden für eine einzelne Bedingungsvariable blockiert und warten auf die Ausführung einer Enqueue-Operation. Wenn enqueue()die Bedingung durch Aufrufen auf true gesetzt wird notifyAll(), werden alle Threads vom Warten befreit (von einer angehaltenen in eine ausführbare Datei verschoben)Zustand). Die freigegebenen Threads versuchen alle, den Monitor auf einmal zu erfassen, aber nur einer würde "gewinnen" und das in die Warteschlange gestellte Objekt erhalten. Das Problem ist, dass die anderen dann auch den Monitor in einer undefinierten Reihenfolge erhalten würden, nachdem der Gewinner-Thread das Objekt aus der Warteschlange genommen und von ihm zurückgekehrt war dequeue(). Da diese anderen Threads nicht erneut auf eine leere Warteschlange testen, wird der Müll aus der Warteschlange entfernt. (Wenn Sie sich den Code dequeue()ansehen, bewegt die Methode den Endzeiger zum nächsten Slot, dessen Inhalt undefiniert ist, da die Warteschlange leer ist.)

Beheben Sie das Problem, indem Sie die ifAnweisung durch eine whileSchleife ersetzen ( Spin Lock genannt ). Jetzt warten die Threads, die das Objekt nicht erhalten, wieder:

public Object synchronized dequeue () {try { while (head == tail) // war früher ein if this.wait (); } catch (InterruptedException e) {return null; } return queue [++ tail% = queue_size]; }}

Diese whileSchleife löst auch ein anderes, weniger offensichtliches Problem. Was ist, wenn wir die notify()Aussage beibehalten und keine einfügen notifyAll()? notify()Wird das Problem nicht gelöst, da nur ein wartender Thread veröffentlicht wird? Es stellt sich heraus, dass die Antwort nein ist. Folgendes kann passieren:

  1. notify() wird vom Enqueueing-Thread aufgerufen und gibt die Bedingungsvariable frei.

  2. Der Warteschlangenthread wird dann vorab freigegeben, nachdem er aus dem Warten auf die Bedingungsvariable freigegeben wurde, aber bevor er versucht, die Sperre des Monitors wieder zu erlangen.

  3. Ein zweiter Thread ruft dequeue()genau an diesem Punkt auf und tritt erfolgreich in den Monitor ein, da keine anderen Threads offiziell warten. Dieser zweite Thread entfernt das Objekt erfolgreich aus der Warteschlange. wait()wird nie aufgerufen, da die Warteschlange nicht leer ist.

  4. Der ursprüngliche Thread darf jetzt ausgeführt werden, er erfasst den Monitor, testet kein zweites Mal auf Leerzeichen und entfernt dann den Müll.

Dieses zweite Szenario lässt sich auf die gleiche Weise wie das erste leicht beheben: Ersetzen Sie die ifAnweisung durch eine whileSchleife.

Beachten Sie, dass die Java-Spezifikation nicht erfordert, dass wait()sie als atomare Operation implementiert wird (dh eine Operation, die beim Übergang von der Bedingungsvariablen zur Sperre des Monitors nicht verhindert werden kann). Dies bedeutet, dass die Verwendung einer ifAnweisung anstelle einer whileSchleife in einigen Java-Implementierungen möglicherweise funktioniert, das Verhalten jedoch wirklich undefiniert ist. Die Verwendung eines Spin Lock anstelle einer einfachen ifAnweisung ist eine günstige Versicherung gegen Implementierungen, die nicht wait()als atomar behandelt werden.

Threads sind keine Objekte

Kommen wir nun zu schwer zu findenden Problemen. Die erste Schwierigkeit ist die alltägliche Verwechslung von Threads und Objekten: Methoden werden auf Threads ausgeführt, Objekte nicht. Anders ausgedrückt, die einzige Möglichkeit, eine Methode für einen bestimmten Thread auszuführen, besteht darin, sie (entweder direkt oder indirekt) über die run()Methode dieses Threads aufzurufen . Es Threadreicht nicht aus , es einfach in die Ableitung zu setzen . Schauen Sie sich zum Beispiel den folgenden einfachen Thread an (der nur ab und zu seine Felder druckt):

Klasse My_thread erweitert Thread {private int field_1 = 0; private int field_2 = 0; public void run () {setDaemon (true); // Dieser Thread hält die App nicht am Leben, solange (true) {System.out.println ("field_1 =" + field_1 "field_2 =" + field_2); Schlaf (100); }} synchronisierte öffentliche Leere modifizieren (int new_value) {field_1 = new_value; field_2 = new_value; }}

Sie können den Thread starten und eine Nachricht wie folgt senden:

My_thread test = new My_thread; test.start (); // ... test.modify (1);