Netzwerkprogrammierung und Threads Teil 2 – V4

Soooo und schon sind wir in der Vorlesung 4. Heute hat die Vorlesung ein wenig chaotisch gestartet, da Herr Noll zunächst zu one Note gewechselt ist und das Ganze dann nicht mehr so geklappt hat, da ein Teil der Folie riesig und der Rest ziemlich klein war. Gewechselt wurde, da in BBC das Zeichnen auf den Folien über den Server läuft und es bei One Note eben lokal ist, aber schlussendlich wurde dann doch wieder zurück gewechselt. Die ersten 10 Folien der Vorlesung hatten wir übersprungen, da das einfach nur die Wiederholung vom letzten Mal war. Danach ging es aber nicht mit den Folien weiter, sondern wir haben gleich mit einer Aufgabe gestartet. Diese war wie folgt:

  1. den folgenden Code abschreiben:

2. Testen

3. Ändern, so dass jeweils erst ein Thread und dann der andere durchläuft

4. So ändern, dass beide abwechselnd durchlaufen

Lustigerweise war beim ersten Mal Testen bei mir die Aufgabe 3 erfüllt, denn erst wurde der Alpha Thread und dann der Beta Thread durchgeführt. Bei weiteren Tests war das dann nicht mehr so, sondern eher so:

und hier die Ausgabe:

zu Aufgabe 3:

und die Ausgabe:

der Thread läuft jetzt einmal ganz durch, bevor der nächste kommt

und hier zu Aufgabe 4:

Mit der Ausgabe:

wie man sehen kann laufe die Threads nun abwechselnd. Leider kann es beim mehrmaligen Testen passieren, dass es nicht immer ganz abwechselnd ist.

Threads haben jedoch auch Nachteile. Sie können zu Nebenläufigkeitsproblemen führen und diese wiederum zu Konkurrenzsituationen. Es kann also passieren, dass zwei oder mehr Threads Zugriff auf die Daten desselben Objekts haben. Das heißt, dass dann Methoden auf zwei verschiedenen Stacks beide ein und dasselbe Objekt auf dem Heap auf. Threads wissen nämlich nicht, wenn sie lauffähig, oder blockiert sind, was passiert.

Als Beispiel hierfür hatten wir „Ehe in Gefahr“. Hier ging es um ein Ehepaar, dass sich das Bankkonto teilt. Beide haben haben vereinbart, dass das Konto nicht überzogen wird. Deshalb überprüfen sie vor der Abhebung den Kontostand. Jedoch ging das ganze schief, denn Rainer (der Mann) brauchte 50 EUR und hat den Kontostand geprüft und gesehen, dass noch 100 EUR drauf waren. Jedoch ist er vor dem Abheben eingeschlafen. Danach möchte die Ehefrau 100 EUR abheben, prüft den Kontostand und es sind noch 100 EUR drauf, also hebt sie den Betrag ab. Da Rainer plötzlich wieder aufwacht und er davon ausgeht, dass sich am Kontostand nichts verändert hat, hebt er die 50 EUR ab und somit ist das Konto überzogen. Das ganze spiegelt die Situation mit den Threads bildlich sehr gut wider. Auch wie ein Thread ist Rainer einfach eingeschlafen und hat beim Aufwachen da weitergemacht, wo er eingeschlafen ist ohne den Kontostand noch einmal zu prüfen.

Um das ganze mal in Codeform zu erklären und zu zeigen:

Der Code hat zwei Klassen BankKonto und RainerUndMonikaJob. Letztere implementiert Runnable und zeigt quasi das Verhalten von Rainer und Monika: also Kontostand prüfen und Abhebung durchführen. In der run()-Methode, wird genau das gemacht, was auch Rainer und Monika machen, nämlich den Kontostand prüfen und Geld abheben, falls genug Geld auf dem Konto ist. Dies sollte eigentlich vor der Überziehung des Kontos schützen, aber wenn Monika und Rainer jedes Mal einschlafen, nachdem sie den Kontostand geprüft haben, dann könnte es zu Problemen kommen.

Wie man sich schon denken kann, führt der oben genannte Code zu Überziehungen. Falls nämlich genug Geld zum Abheben da sein sollte, wird der Thread erst einmal schlafen gelegt, bevor er das Geld abhebt. Nun fragt man sich natürlich, wie man das umgehen kann.

Wie für alles, was man sichern möchte, braucht man ein Schloss. Das würde dann im obrigen Beispiel so aussehen:

  1. Zu der Kontotransaktion gibt es ein Schloss. Es existiert nur ein Schlüssel und der hängt am Schloss, bis jemand auf das Konto zugreifen möchte. Also ist die Transaktion unversperrt, wenn niemand auf das Konto zugreifen möchte.
  2. Wenn z.B. Rainer auf das Konto zugreifen möchte, schließt er ab und behält den Schlüssel. Jetzt kann sonst keiner mehr auf das Konto zugreifen.
  3. der schlüssel wird behalten, bis die Transaktion fertig ist. Daher kann der andere dann nicht zugreifen, bevor der Schlüssel nicht wieder am Schloss hängt. Wenn ein Thread bzw. Rainer oder Monika dann während der Transaktion einschlafen, haben sie noch nie Gewissheit, dass der Kontostand sich in dieser Zeit nicht geändert haben kann.

Man muss also dafür sorgen, dass ein Thread, sobald er einmal mit der Methode abhebungMachen() angefangen hat, diese auch zu Ende ausführt, bevor ein anderer Thread in die Methode eintritt. Oder anders gesagt, man muss sicherstellen, dass einem Thread, der gerade den Kontostand geprüft´hat , garantiert wird, dass er aufwachen und die Abhebung zu Ende ausführen kann, bevor ein anderer Thread den Kontostand prüfen kann. Das geht mit dem Schlüsselwort synchronized.

In den Folien wurde die Methode abhebungMachen() als unteilbare atomare Einheit beschrieben. Herr Noll meinte jedoch, dass das nicht gerade die beste Beschreibung sei und wir es eher als un unterbrechbar betrachten.

Das Schlüsselwort synchronized bedeutet, dass ein Thread einen Schlüssel braucht, um auf den synchronisierten Code zugreifen zu können. Um also die Daten, wie z.B. das Bankkonto zu schützen, synchronisiert man die Methoden, die auf diese Daten einwirken. Hat also ein Objekt eine oder mehrere synchronisierte Methoden, kann ein Thread erst dann in eine synchronisierte Methode eintreten, wenn er den Schlüssel zum Schloss hat. Jedes Java-Objekt hat ein Schloss und das Schloss hat nur einen Schlüssel. Meist ist das Schloss unversperrt und es interessiert keinen, außer, wenn ein Objekt synchronisierte Methoden enthält.

Danach kamen wir auf ein weiteres Problem zu sprechen und zwar das Problem der verlorenen Aktualisierung. Bei der verlorenen Aktualisierung geht es um einen bestimmten Vorgang:

  1. Kontostand abfragen: int i = kontostand;
  2. Kontostand um 1 erhöhen: kontostand = i +1;

Man zwingt hier den Computer die Veränderung des Kontostands in 2 Schritten vorzunehmen (normalerweise würde man kontostand++ schreiben). Dadurch, dass der Schritt in zwei aufgeteilt wird, wird das Problem eines nicht atomaren Vorgangs klar. So gibt es auch komplexere Schritte, die sich nicht in einer Anweisung zusammenfassen lassen.

Beim folgenden Code, der das Problem aufzeigt, sind zwei Threads enthalten, die beide den Kontostand erhöhen.

Führt man den Code aus, dann:

  1. Thread A läuft eine Weile: dann ist i gleich 0 zu Beginn, dann setzt man den Wert des Kontostands auf das Ergebnis von i + 1, dann ist der Kontostand 1. Dann setzt man wieder den Wert des Kontostands in die Variable i, also ist i gleich 1 und dann wird der Wert auch wieder auf das Ergebnis von i + 1 gesetzt, also 2.
  2. Thread B läuft eine Weile:der Wert des Kontostands kommt wieder in die Variable i. Dann setzt man den Wert des Kontostands auf das Ergebnis von i+1. Der Kontostand ist dann 3. Also ist dann i gleich 3. Nun wird Thread B wieder in den Zustand lauffähig versetzt, bevor er den Wert des Kontostands auf 4 setzen kann.
  3. Thread A läuft wieder: setzt den Wert des Kontostands in der Variable i. Der Kontostand ist dann auch wieder 3. Dann setzt man den Kontostand wieder auf das Ergebnis von i + 1. Der Kontostnd ist dann 4. Dann setzt man wieder den Kontostand auf i, also 4. Danach wird der Kontostand auf i+1 gesetzt also 5.
  4. Thread B läuft weiter und fährt dort fort, wo er aufgehört hat. Also bei i = 3 und dann wird wieder i um eins erhöht also 4.

Thread B macht also die Aktualisierung von Thread A zunichte.

Um dem zu umgehen, muss an also hier auch wieder das Schlüsselwort synchronized benutzen:

Nun werden die beiden Methoden zu einer unteilbaren Einheit.

Jedoch sollte man beachten, dass alles zu synchronisieren auch keine Lösung ist, denn dann würde die Performance eingeschränkt werden. Es kann aber auch zu einem Deadlock führen, das heißt, sie können sich gegenseitig blockieren. Man sollte sich bei der Synchronisierung also auf das Notwendigste beschränken.

Wenn eine Methode synchronisiert ist könnte das wie folgt ablaufen:

Thread A versucht in die Methode inkrementieren() einzutreten. Die Methode ist synchronisiert, also holt Thread A sich den Schlüssel für dieses Objekt. Dann wird die Methode ausgeführt und Thread A gibt anschließend den Schlüssel wieder zurück. Falls Thread A länger dran ist, wird er die Methode noch einmal ausführen. Falls er dann die Methode nicht zu ende ausgeführt hat, bevor es in den Zustand lauffähig gesetzt wird, behält er den Schlüssel. Wenn er dann wieder weiterläut, kann er die Methode vollbringen und dann den Schlüssel wieder zurück geben.

Nun zu einem dunkleren Thema: die tödliche Seite der Synchronisierung (nur dezent übertrieben :D). Wie schon erwähnt, sollte man nur synchronisieren, wenn dringend notwendig und vor allem nur das Minimum, denn es könnte zu einem Deadlock führen. Diese Blockierung kann daher kommen, dass zwei Threads jeweils einen Schlüssel festhalten, den der andere haben möchte. Es gibt keinen Weg, der aus der Situation herausführt, deshalb warten die Threads einfach. Für einen Deadlock reichen bereits zwei Threads und zwei Objekte aus.

In der Vorlesung haben wir dann die nächsten Folien (45-47) weitestgehend übersprungen, denn das sind auch zum Großteil Wiederholungen. Jedoch habe ich mir die Punkt für Punkt, das sind die Wiederholungen zu den Folien, noch einmal durchgelesen und die Punkte, die ich gut erklärt fand und mir beim Verständnis geholfen hatten, liste ich nun auf:

  • Um ein Objekt threadsicher zu machen, legt man fest, welche Anweisung als ein atomarer Prozess behandelt werden soll. Das heißt, dass man festlegt, welche Methoden vollständig ablaufen müssen, bevor ein anderer Thread Zutritt zur selben Methode desselben Objekts erhält
  • Wenn an zwei Threads darin hindern möchte, gleichzeitig eine Methode zu benutzen, dann benutzt man das Schlüsselwort synchronized
  • Selbst wenn ein Objekt mehr als eine synchronisierte Methode hat, gibt es nicht mehr als einen Schlüssel ( macht ja auch Sinn, sonst könnten die Daten ja dennoch manipuliert werden)

Danach kamen wir wieder auf den Code zu sprechen, der in das Thema der Threads eingeleitet hatte: nämlich der EinfacheChatClient. Dieser sollte ja die Nachrichten an den Server senden und empfangen, was aber nicht möglich war, da wir die Threads noch nicht kannten.

Dazu hatten wir erst einmal folgendes Bild gemalt, was beschreiben soll, wie der Informationsfluss aussieht:

Der Code sieht wie folgt aus:


hier kann man noch die Zeichnung sehen, die ich während des Unterrichts hinzu gezeichnet habe. Im Anschluss werde ich das noch ein wenig ordentlicher malen und erklären.
hier sind wir sehr schnell drüber, da hier nicht viel Neues drin steckt

Zu der GUI (also zu dem 2. Bild und zur Ergänzung, falls jemand nicht weiß, was GUI heißt: Graphical User Interface) hier noch das etwas ordentlichere Bild:

Hier zu sehen ist das JPanel also das innere Fenster, mit der Textarea (hier das mit den Linien), einem Textfield mit der Länge 20 (siehe Code) und einem senden Button.

Nach dem Code gibt es außen herum noch das JFrame, oder anders gesagt, das Fenster.

Im Gegensatz zum vorherigen Code, kann man hier sehen, dass ein neuer Thread gestartet wird, dessen Aufgabe es ist, den Socket-Strom des Servers zu lesen und alle eingehenden Nachrichten im scrollbaren Textbereich anzuzeigen. fScroller innerhalb der GUI steht für den Scrollbalken.

Anschließend kam dann, wie zu erwarten, auch der Server zu dem Client.

hier auch wieder eine kleine Hilfestellung als Zeichnung von mir

falls man Client und Server (ist ja auch sinnig, da zum Funktionieren beides gebracuht wird) zusammen testen möchte, sollte man zunächst den Server und dann den Client laufen lassen. Dies ist auch sinnig, da der Server ja erst einmal da sein muss, bevor er etwas empfangen kann.

Da ich ab diesem Zeitpunkt aufgrund eines Termins nicht mehr da war, musste ich noch einmal nachschlagen, was denn ein Iterator ist. Ein Iterator ist ein spezieller Zeiger, der innerhalb eines Programms vom Software-Entwickler dazu verwendet werden kann, um auf Elemente einer Menge, vereinfacht eine Liste, zuzugreifen. Iteratoren arbeiten nach dem Grundprinzip :Wenn es ein weiteres Element in der Liste gibt, dann stelle es zur Verfügung. Wie man oben im Code sieht, gibt es eine Array Liste, die die Ausgabeströme des Clients enthält. Der Iterator iteriert also so lange, bis keine Einträge mehr in der Liste vorhanden sind. Die zur Hilfe gezeichnete Stütze, zeigt ein Array, bei dem immer die nächste Nachricht geschrieben und danach die Nachricht ausgegeben wird.

Hier auch noch einmal ein Bild, welches wir zusammen im Unterricht zu Hilfe gemalt hatten:

Wie man sehen kann, gibt es eine ArrayListe, die die client Ausgabeströme enthält. Der Server auf Port 5000 wird zunächst einmal vor der while Schleife gestartet. In der while Schleife wartet dann der Server auf eine Anfrage, damit er sich verbinden kann und falls diese da ist, nimmt er sie an. Danach holt er sich über den erstelleten PrintWriter die Output-Ströme des Client (bzw. des Sockets des Clients). Anschließend wird die Kommunikation an einen nebenläufigen Thread abgegeben, der dann habe eine Verbindung ausgibt.

Leider löst die Synchronisierung nicht alle Probleme zwecks nebenläufigen Zugriffs. So kann es beispielsweise immer noch zu Problemen kommen, falls ein Thread für die Erzeugung und Speicherung neuer Werte in einem Puffer-Objekt zuständig ist, während ein anderer die dort abgelegten Werte wieder ausliest .

Hierzu wurde in der Vorlesung folgendes aufgemalt:

Hier bin ich jedoch selbst nicht 100% sicher, was es genau bedeutet, denn wie gesagt, war ich nun nicht mehr anwesend und habe die Bilder, die wir im Unterricht aufgemalt hatten nur zugeschickt bekommen. Ich gehe bis jetzt davon aus, dass es für Erzeuger, Verarbeitung und Verbraucher steht, da in dem Kreis des ersten Vs noch ein w, für Wert steht. Ich hoffe am Ende des Nacharbeitens, kann ich genau sagen was es bedeutet, also werde ich später noch einmal darauf eingehen. Mir ist aufgefallen, dass es gerade in diesem Fach wirklich essentiell ist jede Vorlesung aktiv teilzunehmen, da mir es nun merklich schwerer fällt die Themen zu verstehen, wenn man nur die Folien, bzw. nur Zeichnungen hat, ohne Erklärungen.

In den Folien wurde das oben genannte Problem durch folgende Problemstellung verdeutlicht: Ein Thread, der der Erzeuger ist, soll dafür zuständig sein, die ganzzahligen Werte 0 bis 4 zu erzeugen und in einem Vermittler-Objekt abzulegen, während ein weiterer Thread (= der Verbraucher) diese Werte aus dem Vermittler-Objekt entsprechend auslesen soll. Zur Darstellung eines solchen Vermittlerobjekts wurde eine spezifizierte abstrakte Klasse spezifiziert:

Ein Wert-Objekt stellt einen sehr kleinen Puffer dar, weil es es nur einen int-Wert in der Instanzvariable wert aufnehmen kann (siehe Zeile 4). Dieser Wert soll mit put() gestezt und mit get() ausgelesen werden können(Zeile 3 und 4).

Dies ist der Code, der für den Erzeuger Thread:

Das ist die von uns aufgezeichnete Zeichnung hierzu

wie man sehen kann wir in der run Methode des Threads eine for-Schleife durchlaufen, bei der die Werte 0 bis 4 mit Hilfe der Methode put im Vermittler-Objekt eintragen. Jedoch ist mir aufgefallen, dass unsere Zeichnung nicht ganz stimmt, denn die 5 ist hierbei nicht inkludiert.

Zum Erzeuger gehört natürlich auch ein Verbraucher. Also auch hier war der folgende Code gegeben:

auch hier wieder das passende Bild

dieser Thread wird später mit dme gleichen Wert-Objekt arbeiten. Er liest in seiner run Methode die fünf dort abgelegten Werte der Reihe nach aus und protokolliert sie auf dem Konsolenfenster. Beide Threads sind so implementiert, dass sie nach dem put bzw. get-Aufruf für 0 bis 100 Millisekunden schlafen gelegt werden (z. 10 bei Thread 1 und 11 bei Thread 2).

Für das erste Testprogramm:

hat man den Erzeuger und Verbraucher mit einem Wert-Objekt der folgenden Klasse arbeiten lassen, in der die Methoden get und put als synchronized deklariert wurden:

Wenn man das Programm EVTest1 nun startet passiert folgendes:

Der Erzeuger produziert zwar die Werte 0 bis 4 in der richtigen Reihenfolge, der Verbraucher entnimmt dem Vermittlerobjekt aber einige Werte mehrfach und manche gar nicht. So wird die 1 mehrfach ausgegeben, die 2 aber 3 hingegen gar nicht. Um das zu beheben, muss man dafür sorgen, dass der Verbraucher nur dann aktiv wird, wenn der Erzeuger einen neuen Wert bereitgestellt hat. Die beiden Threads müssen sich also irgendwie absprechen bzw. kommunizieren.

Methoden für die Thread Kommunikation:

  • public final void join (): veranlasst den gerade ausgeführten Thread mit seiner weiteren Ausführung so lange zu warten, bis der Thread, für den join ausgeführt wird, beendet ist.
  • public final void join (long millis): genau das Gleiche wie die obere Methode, außer, dass dr Thread, der gerade ausgeführt dazu veranlasst wird, mit seiner weiteren Ausführung maximal millis Millisekunden zu warten
  • public final void wait(): Veranlasst den Thread, der gerade ausgeführt wird, mit seiner weiteren Ausführung zu warten, bis ein anderer Thread die notify– oder die notifyAll-Methode für das aktuelle Objekt ausführt
  • public final void wait (long timeout): dasselbe wie eins oben drüber, nur dass der Thread, der ausgeführt wird mit seiner weiteren Ausführung maximal timeout Millisekunden zu warten.
  • public final void notify(): reaktviert einen einzelnen Thread, der sich im Wartezustand bezüglich des aktuellen Objekts befindet
  • public final void notifyAll(): dasselbe, wie die obere Methode, nur dass alle Threads reaktiviert werden

Für den Problemfall oben ist die Methode join nicht nützlich, denn der Verbraucher Thread möchte ja nicht erst wieder aktiv werden, wenn der Erzeuger Thread bereits beendet ist. Also bleiben nur noch wait und notify übrig.

Hier der in den Folien gezeigte Code:

diese Klasse ist die Unterklasse der Klasse wert, was man durch das Schlüsselwort extends sehen kann. Das Vermittlerobkjekt arbeitet mit einem Flag verfuegbar, das anzeigt, ob bereits ein Wert vom Erzeuger produziert und damit für den Verbrauch bereitgestellt wurde. In der Methode get lässt man den Verbraucher Thread, der die Methode ausführt, daher zunächst überprüfen, ob ein Wert verfügbar ist (z.4). Wenn das nicht der Fall sein sollte, dann lässt man ihn mit wait (z.6) erst mal auf ein notify vom Erzeuger warten. Wird der Wartezustand aufgehoben (Z.11) , steht fest, dass ein neuer Wert in der Variable wert vorhanden ist und nun verbraucht werden kann. Durch den notify Aufruf informiert der Verbraucher den Erzeuger, dass er wieder aktiv werden soll und verbraucht den aktuellen Wert mit return (Z.13). In der Methode put lässt man den Erzeuger Thread, der die Methode ausführt, zunächst überprüfen, ob ein Wert verfügbar ist (z.15f). Wenn ja, dann lässt man ihn mit wait (z.18) auf ein notify vom Verbraucher warten. Wird das Warten aufgehoben, dann kann ein neuer Wert für die Variable wert eingetragen werden. Nach der Speicherung des neuen Werts (z. 22f) wird nun das verfuegbar Flag gesetzt (z.24) und der Verbraucher durch einen notify-Aufruf (z.25) darüber informiert, dass er wieder aktiv sein kann.

Wenn die beiden Threads dann im Rahmen dieses Programms (s.u.) mit einem Objekt der neuen Klasse GuterWert arbeiten lassen, so bekommt man folgende Ausgaben:

hier das Programm
hier die Ausgabe

Um das Ganze noch einmal zusammenzufassen kann man also sagen, dass durch die Verwendung des Schlüsselworts synchronized und den Methoden join, wait, notify und notifyAll das Problem der Nebenläufigkeit in kritischen Situationen gelöst wurde. Jedoch sollte man auch hier sorgfältig sein, denn es könnte auch zu einer Deadlock-Situation führen.

Da ich mich ehrlich gesagt nicht mehr ganz daran erinnern konnte was ein Flag ist, und ich ja wie gesagt in dieser Zeit nicht mehr an der Vorlesung teilnehmen konnte hatte ich nachgeschaut und ein Flag ist eigentlich nichts anderes, als ein Signal, welches dem Programm mitteilt, dass eine bestimmte Bedingung erfüllt ist. Sie fungiert normalerweise als boolesche Variable, die anzeigt, dass eine Bedingung entweder wahr oder falsch ist. Also nicht Neues, sondern lediglich ein anderer Begriff. Hätte man den Code oben ohne die Flag geschrieben, hätte dies womöglich zu einem Deadlock geführt.

Bloggen auf WordPress.com.

Nach oben ↑

Erstelle eine Website wie diese mit WordPress.com
Jetzt starten