Ich darf Sie alle recht herzlich zur letzten Übung in diesem Semester
begrüßen.
Freut mich, dass doch noch ein paar von Ihnen gekommen sind, immerhin.
Zu Beginn möchte ich noch eine Ankündigung machen, die betrifft die
Klausur, beziehungsweise die Abmeldung von der Klausur.
Das heißt, wenn Sie sich schon angemeldet haben und entschließt sich
aus irgendeinem Grund plötzlich doch nicht mehr teilnehmen zu wollen,
dann müssen Sie sich abmelden von der Klausur.
Und zwar können Sie sich abmelden bis drei Arbeitstage vor
Klausurbeginn.
Das heißt, es wäre der Montag nächste Woche.
Bis dann können Sie sich noch ohne Gründe abmelden.
Wenn Sie sich danach abmelden möchten, dann brauchen Sie dazu einen
driftigen Grund, den Sie nicht selbst zu vertreten haben.
Zum Beispiel, wenn Sie krank sind und ein ärztliches Attest vorweisen
können, können Sie sich auch noch später abmelden.
Sie sollten sich persönlich abmelden, auf den einfachen Grund, weil
Sie dann den blauen Schein zurückbekommen.
Falls Sie aus irgendeinem Grund sich nicht persönlich abmelden können,
zum Beispiel wegen Krankheit, dann können Sie sich auch vorläufig
abmelden, indem Sie anrufen oder eine E-Mail schreiben, aber das
müssen dann trotzdem noch vorbeikommen, sobald Sie wieder gesund sind,
um Ihren Schein abzuholen.
Dann kommen wir zur ersten Aufgabe auf dem letzten Übungsblatt.
Da ging es um die Ausnahmebehandlung.
Und in der ersten Teilaufgabe sollten Sie sich Gedanken machen, was
sind die Unterschiede zwischen der Fehlerbehandlung durch Ausnahmen
gegenüber der Fehlerbehandlung durch Rückgabe Codes.
Zum einen können Sie, wenn Sie Ausnahmen definieren, die
Fehlerbehandlung vom normalen Programmablauf trennen.
Das schauen wir uns ein Beispiel an.
Hier haben wir ein Programm, da werden drei Methoden aufgerufen, M1
bis M3.
Und jeweils nach dem Aufruf der Methode fragt das Programm den
Rückgabe -Code ab.
Und falls dieser Rückgabe-Code kleiner Null ist, was einen Fehler
anzeigen soll, dann wird eben diese Fehlerbehandlungsroutine
aufgerufen.
Wenn wir dazu im Vergleich das selbe Programm betrachten, das mit
Ausnahmen funktioniert, dann sehen Sie hier, im Try-Block gekapselt
werden die drei Methoden nacheinander aufgerufen und im Catch-Block
werden mögliche Fehler behandelt.
Also der normale Programmablauf geht immer davon aus, dass alles gut
geht und erst ganz am Schluss schreibe ich auf, was mache ich, wenn
ein Fehler unterwegs aufgetreten ist.
Das heißt, ich muss nicht dauernd den Programmablauf unterbrechen,
durch solche Abfragen ist ein Fehler aufgetreten.
So kann ich mein Programm dadurch besser strukturieren und es wird
leichter lesbarer.
Ein weiterer Vorteil von Ausnahmen, Ausnahmen werden garantiert
behandelt.
Dafür sorgt mein Laufzeitsystem und mein Compiler.
Wenn ich eine Methode aufrufe und die kann eine Ausnahme zurückgeben
und ich fange diese Ausnahme nirgends ab und behandle es nirgends,
dann lässt sich das Programm gar nicht übersetzen.
Während bei so einem Rückgabecode, da garantiert mir niemand, dass der
auch abgefragt wird.
Ich könnte diese Programmzeile hier auch komplett weglassen, das
Programm würde sich immer noch übersetzen lassen und ich würde niemals
rausfinden, wenn der Methode im 1 Fehler aufgetreten ist, weil ich
eben den Code nicht anschaue.
Während Ausnahmen, wenn die Methoden so deklariert sind, dass sie sich
auslösen können, dann muss ich sie auch behandeln.
Der dritte Vorteil, Ausnahmen sind flexibler als Rückgabecodes, denn
ich habe die Möglichkeit, Ausnahmen weiterzugeben.
Das heißt, ich muss sie nicht an Ort und Stelle behandeln, sondern
kann sagen, ich gebe sie in der Aufrufkette in die nächste Methode
weiter.
Das sieht dann so aus, anstelle, dass ich hier dieses Catch-Statement
habe, könnte ich den Aufruf in eine eigene Methode kapseln, M4, und
gebe an, diese Methode kann die Error-Exception auslösen.
Allerdings hier im Methoden-Rumpf löst sich diese Exception nirgends
aus.
Der Grund, warum ich hier angeben muss, sie kann die Error-Exception
auslösen, ist, dass eine dieser Methoden im 1 bis in 3 dich aufrufe,
diese Ausnahme auslösen kann, und die gebe ich dann weiter in die
Methode, die die Methode M4 aufgerufen hat, und ich kann sie dann an
dieser Stelle behandeln.
Ich bin also flexibel und kann irgendwo in meiner Aufrufkette einen
Fehler behandeln.
Man muss ihn nicht an der Stelle behandeln, in der er auftritt.
Das waren so die drei größten Vorteile, die Ausnahmen gegenüber
Rückgabecodes aufweisen.
Dann, im zweiten Teil der Aufgabe sollten Sie aufzählen, was für Arten
von Ausnahmen in Java unterschieden werden.
Da haben wir zum einen die System-Aushaben.
Die werden vom Laufzeitsystem ausgelöst oder vom Betriebssystem als
Laufzeitsystem weitergegeben und entstehen, wenn das Programm
irgendwelche Anweisungen ausführt, die es nicht ausführen dürfte.
Zum Beispiel, wenn eine Division durch 0 erfolgt, wird eine Ausnahme
ausgelöst.
Wenn auf eine Objektreferenz benutzt wird, die ungültig ist, weil zum
Beispiel 0 drinsteht, wird eine Ausnahme ausgelöst.
Ein anderes Beispiel, wenn ich auf ein Feld zugreife, außerhalb der
gültigen Feldgrenze, zum Beispiel habe ich ein Feld definiert, das
soll 100 Elemente haben und ich greife das 101.
zu, dann gibt es dieses Element nicht und es wird eine Ausnahme
ausgelöst.
Das andere sind die sogenannten Benutzerausnahmen.
Das sind alle Ausnahmen, die ausschließlich durch eine Throw-Anweisung
vom Benutzer ausgelöst werden können und nicht einfach so auftreten.
Solche Benutzerausnahmen kann ich auch selbst festlegen als
Programmierer und kann mir jede beliebige Ausnahme damit definieren.
Beispiele wären, wenn der Benutzer zum Beispiel eine Zahl eingeben
soll und gibt aber einen String ein und mit Zeichenfolge kann ich das
überprüfen und im Fehlerfall eine Ausnahme auslösen.
Oder wenn ich eine Methode anbiete zum Öffnen von einer Datei und der
Benutzer gibt einen Dateinamen an und diese Datei ist aber nicht
vorhanden, kann meine Methode eine Ausnahme zurückliefern, um das
anzuzeigen.
Ein drittes Beispiel, da haben wir nachher auch noch eine Aufgabe, die
das behandelt ist.
Wenn ein falsches Passwort eingegeben wurde, kann ich das auch über
Ausnahmen behandeln.
Im letzten Teil der ersten Frage, da ging es um die Finally-Klausel.
Wenn ich so einen Try-and-Catch-Block habe, kann ich hinten noch eine
Finally -Klausel anhängen und die Frage ist jetzt, in welchem Fall
werden die Anweisungen, die in dieser Klausel enthalten sind,
ausgeführt.
Und die Antwort ist, sie werden in jedem Fall ausgeführt.
Das heißt, egal ob eine Aufnahme aufgetreten ist oder nicht, was in
der Finally-Klausel drin steht, wird ausgeführt.
Und zu welchem Zeitpunkt, es hängt davon ab, ob eine Ausnahme
aufgetreten ist oder nicht.
Wenn eine Ausnahme aufgetreten ist, dann wird direkt nach Beendigung
des entsprechenden Catch-Blocks in den Finally-Block gesprungen.
Das heißt, dann wird der Finally-Block nach der letzten Catch-Klausel
ausgeführt.
Falls keine Ausnahme aufgetreten ist, dann wird der Finally-Block nach
Abschluss des Try-Blocks ausgeführt.
Das heißt, dann springt er nach dem Try-Block hier in den Finally
-Block rein.
Und was ist der Sinn?
Warum gibt es überhaupt diese Finally-Klausel?
Darin kann ich Arbeiten beenden, die ich im Catch-Block angefangen
habe, wenn ich im Try-Block angefangen habe.
Wenn ich z.B.
hier im Try-Block eine Datei geöffnet habe und so eine Open-Methode
aufgerufen...
...Datei geöffnet habe, kann ich die im Finally-Block wieder
schließen, wenn ich hier Close aufrufe.
Was ist der Sinn da davon?
Warum kann ich nicht einfach im Try-Block hier Close aufrufen?
Und im Fall, wenn keine Ausnahme auftreten würde, würde das auch
funktionieren.
Aber wenn ich jetzt hier eine Ausnahme habe mittendrin, dann springt
er mir in die Catch-Klausel und danach weiter.
Dann wird das Close nie ausgeführt.
Deswegen gibt es diese Finally-Klausel, die in jedem Fall ausgeführt
wird.
Dann kommen wir zur zweiten Aufgabe.
Da sollten Sie mal das Konzept der Ausnahmen praktisch anwenden, indem
Sie eine Passwortüberprüfung und Passwortverwaltung implementieren.
Und im ersten Aufgabenteil ging es darum, erstmal eine Ausnahme zu
definieren, die auftritt, wenn ein falsches Passwort eingegeben ist.
Und dann die Klasse Passwort zu implementieren, die Methoden anbietet
zum Überprüfen von Passwörtern und zum Ändern von Passwörtern.
Und dann natürlich noch ein Konstruktor, bei dem man das anfängliche
Passwort setzen kann.
Zunächst zur Ausnahme.
Da definieren wir uns eine Klasse WrongPasswordException.
Und das wird der Ausnahme dadurch, dass ich von der Klasse Exception
erbe.
Also hier durch dieses ExtentsException.
Und zusätzliche Attribute brauche ich aber keine hinzufügen, das ist
einfach nur ein neuer Name für meine Exception.
Wir haben nachher noch den Fall, wo ich wirklich Veränderungen an der
Klasse Exception vornehme, nachdem ich geerbt habe.
Dann implementieren wir unsere Klasse Passwort.
Was brauchen wir da dazu für Attribute?
Natürlich unser Passwort.
Das ist ein einfacher String und der soll natürlich private sein.
Denn es nützt mir nichts, wenn jeder mein Passwort lesen kann.
Dann bauen wir uns den Konstruktor zum Setzen eines initialen
Passworts.
Das heißt, wenn ich ein neues Objekt der Klasse Passwort anlege, dann
gebe ich an, was soll das anfängliche Passwort sein.
Es nimmt als Argument einen String und weist dann dem Attribut
Passwort diesen String als Wert zu.
Dann brauchen wir noch eine Methode zum Überprüfen des Passworts, da
übergebe ich wieder einen String.
Und überprüfe, ob mein Passwort mit diesem String übereinstimmt.
Das mache ich mit dieser Equals Methode.
Und falls das Passwort nicht übereinstimmt, dann gebe ich eine Wrong
Password Exception aus.
Und das muss ich auch in der Methodendeklaration angeben, dass diese
Methode diese Wrong Password Exception ausgeben kann.
Als letztes noch unsere Methode zum Ändern des Passworts.
Da gebe ich als Parameter mit das alte Passwort, damit der Benutzer
zeigen kann, ich bin berechtigt dazu, das Passwort zu ändern.
Und das neue Passwort, auf das das Passwort gesetzt werden soll.
Und was diese Methode im Wesentlichen macht, ist, sie weist dem
Passwort das neue Passwort zu.
Allerdings, bevor ich das mache, sollte ich erstmal überprüfen, ob
dieses alte Passwort überhaupt stimmt.
Also ob der Benutzer das richtige alte Passwort angegeben hat.
Und dazu benutze ich meine Check Methode, die ich ja schon definiert
habe.
Allerdings, diese Check Methode, haben wir vorhin gesehen, kann die
Wrong Password Exception ausgeben.
Und die will ich aber nicht in der Change Methode behandeln, sondern
die gebe ich einfach weiter.
Das heißt, ich sage auch bei der Change Methode, die kann die Wrong
Password Exception ausgeben.
Und falls diese Ausnahme durch die Check Methode ausgelöst wird, gibt
die Change Methode die einfach weiter und lässt den Aufrufer die
Ausnahme behandeln.
Gut, ist diese Klasse Passwort soweit klar?
Sieht so aus, dann können wir weitergehen.
Im zweiten Teil sollen Sie diese Klasse erweitern, und zwar um eine
Sicherheitsüberprüfung des Passworts.
Und da ist gefordert, dass ein Passwort mindestens fünf Zeichen
enthalten soll.
Einen Großbuchstaben, einen Kleinbuchstaben, eine Ziffer und ein
Sonderzeichen.
Eben die üblichen Dinge, die dafür sorgen, dass man sein Passwort am
nächsten Tag schon wieder vergessen hat.
Was muss ich dazu machen?
Zunächst definieren wir uns die Ausnahme, die auftreten soll.
Was ein Passwort nicht sicher ist.
Und da war gefordert, dass aus der Ausnahme ersichtlich werden soll,
warum das Passwort nicht sicher ist.
Und deswegen führen wir zuerst mal die fünf Attribute ein.
Und zwar sind es boolische Attribute, die true oder false enthalten,
je nachdem ob das Passwort zu kurz war.
Keine Kleinbuchstaben, keine Großbuchstaben, keine Ziffern und keine
Sonderzeichen enthalten hat.
Dann bauen wir einen Konstruktor, mit dem ich, wenn ich so eine neue
Ausnahme anlege, diesen Attributen Werte zuweisen kann.
Also ich übergebe einfach als Parameter, was soll ich diesen
Attributen zuweisen.
Und der Konstruktor macht dann nichts anderes als jedem einzelnen
Attribut einen Wert zuzuweisen.
Und da die Parameter und die Attribute die gleichen Bezeichnungen
haben, muss ich die Attribute mit diesem this Schlüsselwort
kennzeichnen.
Und dann bietet meine Klasse noch die Möglichkeit abzufragen, was war
denn der Grund, warum mein Passwort nicht akzeptiert worden ist.
Das sind einfach fünf Methoden, die den Wert der Attribute
zurückgeben.
Da kann ich nachfragen, war mein Passwort zu kurz, waren in meinem
Passwort keine Großbuchstaben vorhanden und so weiter.
Also sie tun nichts anderes als die Werte der Attribute zurückgeben.
Ich denke, das ist auch noch verständlich.
Gehen wir weiter.
Dann muss ich natürlich noch eine Methode definieren in meiner Klasse
Passwort, die mir erlaubt zu überprüfen, ob ein Passwort sicher ist
und im anderen Fall diese Ausnahme ausgibt.
Und die nenne ich CheckSecure.
Da übergebe ich ein mögliches Passwort, das ich überprüfen will und
gebe eventuell diese InsecurePasswordException, die wir gerade
definiert haben, aus.
In dieser Methode lege ich mir wieder meine fünf Werte fest.
Ist mein Passwort lang genug, hat es Großbuchstaben, hat es
Kleinbuchstaben, hat es Ziffern und Sonderzeichen.
Und dann fange ich an.
Erstmal ist mein Passwort lang genug, wenn es mehr als fünf Zeichen
enthält.
Das prüfe ich zuerst.
In alle anderen Attribute setze ich erstmal auf False.
Das heißt, ich gehe davon aus, dass es keine Großbuchstaben drin sind,
keine Kleinbuchstaben und so weiter.
Dann baue ich mir eine Vorschleife und gehe durch jedes einzelne
Zeichen von meinem Passwort durch.
Ich zähle also von 0 bis zur Länge meines Strings.
Und schaue mir jedes einzelne Zeichen an, definiere mir eine Variable
C und ziehe jeweils mit CharacterAdd ein Zeichen raus aus meinem
String und untersuche dieses Zeichen.
Und schaue nach, ob das ein Großbuchstabe ist und wenn ja, dann setze
ich uppercase diese Variable auf True.
Wenn es ein Kleinbuchstabe ist, setze ich die entsprechende Variable
auf True.
Wenn es eine Ziffer ist, setze ich diese Digit-Variable auf True.
Und wenn es keins von den dreien war, dann muss es wohl ein
Sonderzeichen gewesen sein.
Deswegen setze ich im letzten l-Zweig diese SpecialCharacter-Variable
auf True.
Und dann bin ich fertig.
Dann muss ich nur noch schauen, ob alles erfüllt war, was ich
gefordert habe.
Also, wenn nicht, sowohl das Passwort lang genug war, Großbuchstaben,
Kleinbuchstaben, Ziffern und Sonderzeichen erhalten hat.
Also, wenn das alles nicht erfüllt ist, dann gebe ich eine Ausnahme
raus und übergebe als Parameter wieder die entsprechenden Werte.
Hier verneint, weil ich hatte ja hier den positiven Fall betrachtet.
Gut, das war die Überprüfung, ob ein Passwort sicher ist.
Diese Zuweisung hier ist nicht long enough hier.
Dieses Ausrufezeichen kehrt praktisch die Bedeutung der Variable um.
Also, wenn in diesem long enough True drin steht, dann ist
Ausrufezeichen long enough false.
Also, es ist eine Verneinung.
Weil ich habe ja hier überprüfig, ob die Länge größer 5 ist und wenn
es lang genug ist, dann setze ich das auf True.
Und in der Ausnahme will ich aber wissen, war es zu kurz.
Deswegen muss ich das verneinen.
Okay?
Weitere Fragen?
Dann können wir weitergehen.
Wie bauen wir das jetzt in unsere Klasse Passwort ein?
Also, hier habe ich nochmal unverändert die Klasse Passwort, so wie
wir sie vorhin implementiert haben.
Und jetzt wollen wir diese erweitern um die Sicherheitsprüfung.
Nun, welche Stellen muss ich da verändern?
Zunächst muss ich mir ja den Konstruktor anschauen, weil da weiß ich
ja zunächst das erste Passwort zu und jetzt will ich wissen, ist
dieses Passwort überhaupt sicher.
Deswegen füge ich hier einen Aufpuff an CheckSecure ein, um zu
überprüfen, ob ich überhaupt ein sicheres Passwort erzeugen will.
Und wenn nicht, dann erzeugt mir CheckSecure eine Ausnahme und die
gebe ich einfach weiter.
Also sagt mein Konstruktor, kann es diese InsecurePasswordException
ausgeben und die wird eben erzeugt, wenn dieses CheckSecure
fehlschlägt.
Und in diesem Fall verlasse ich den Konstruktor und weise den Passwort
nicht in Wert zu.
Ähnlich muss ich das bei der Change-Methode machen.
Da füge ich wieder ein, CheckSecure und sage, die Methode soll
zusätzlich noch die InsecurePasswordException ausgeben können.
Also Sie sehen, durch ganz wenige Veränderungen kann ich meine Klasse
erweitern um die Sicherheitsüberprüfung.
Das ist eben der Vorteil von Exception, die sind flexibel.
Ich kann einfach sagen, gebe die Exception weiter und der Aufrufer
soll sich darum kümmern.
Deswegen komme ich hier mit so wenigen Änderungen aus.
Dann sollten Sie das Ganze noch verwenden in einer Main-Methode, die
Passwörter erzeugt und ändert.
Da habe ich meine Main-Methode und dann baue ich meinen Drive-Log und
sage, ich möchte jetzt erstmal ein Passwortobjekt erzeugen.
Ich gebe eine Meldung an den Benutzer aus, bitte gib ein Passwort ein,
liese das mit Readline ein und erzeuge mir dann ein neues
Passwortobjekt.
Dann will ich mein Passwort überprüfen, gebe wieder die Meldung aus,
überprüfe das Passwort, liese ein Passwort ein mit Readline und rufe
die Check-Methode des Passwortobjekts auf.
Danach gebe ich aus, das Passwort war korrekt.
Warum kann ich das machen?
Das Passwort hätte ja auch nicht korrekt sein können.
Nun, ich gehe immer davon aus, im Drive-Log alles geht gut.
Und wenn etwas schief geht, dann springe ich automatisch in den Catch
-Block.
Das heißt, dann verlasse ich den Drive-Log hier an dieser Stelle und
die nächste Anweisung wird nicht mehr ausgeführt.
Das heißt, wenn ich an diese Stelle komme, dann war mein Passwort auch
korrekt.
Um die Fehlerbehandlung kümmere ich mich an anderer Stelle.
Das mache ich nach Abschluss des Drive-Logs hier im Catch-Block.
Ich fange die Wrong Password Exemption ab.
Wenn eine solche Exemption aufgetreten ist, dann gebe ich die Meldung
aus, du hast ein falsches Passwort eingegeben.
Was kann auch für eine Ausnahme in diesem Block auftreten?
Es kann auch sein, dass mein Passwort hier am Anfang, als ich das neue
Objekt erzeugt habe, unsicher war.
Deswegen muss ich noch die Insecure Password Exemption abfangen.
Und da gebe ich dann aus, warum mein Passwort unsicher war.
Das heißt, ich prüfe ab, war mein Passwort zu kurz.
Dann gebe ich die entsprechende Meldung aus.
Dann überprüfe ich, hat mein Passwort keine Großbuchstaben gehabt,
gebe die entsprechende Meldung aus und so weiter.
Okay, das war mein Programm zur Passwortüberprüfung.
Gibt es dazu Fragen?
Sieht nicht so aus.
Dann kommen wir zum Thema Threats und Synchronisation.
Da ging es um einen gewissen Dienst, der durch eine Klasse modelliert
wird.
An diesem Dienst sollen sich maximal vier Benutzer anmelden können.
Da ging es darum, zu zeigen, dass es durch mangelnde Synchronisation
möglich ist, dass dieser Dienst ausgedrückt werden kann.
Und dass sich eben doch mehr als vier Benutzer anmelden.
Da sollten Sie in der ersten Aufgabe eine mögliche
Ausführungsreihenfolge finden.
Um was es hier geht, ich habe den entsprechenden Code-Ausschnitt
nochmal hier auf der Folie.
Das ist die Methode LOGON, mit der ich einen zusätzlichen Benutzer
anmelden kann.
Diese Methode überprüft, ist die Anzahl der bisher angemeldeten
Benutzer kleiner als die Anzahl der maximal angemeldeten Benutzer?
Und wenn ja, erhöht sie die Anzahl der Benutzer um 1 und gibt TRUE
zurück.
Andernfalls erhöht sie die Anzahl nicht und gibt FALSE zurück, als
Anzeichen dafür, dass der Benutzer nicht angemeldet werden konnte.
Wie kann ich das jetzt austricksen?
Nehmen wir an, es haben sich schon drei Benutzer korrekt angemeldet.
Und jetzt habe ich zwei Kontrollfäden, THREAD1 und THREAD2.
Und die wollen jeweils einen weiteren Benutzer anmelden.
Fängt THREAD1 an, ruft die LOGON-Methode auf, um den Benutzer zu
registrieren.
Innerhalb von der Methode erfolgt jetzt die Abfrage, ist die Anzahl
der angemeldeten Benutzer kleiner als die Anzahl der maximalen
Benutzer?
Und dieses numUsers hat im Moment den Wert 3 und das maxUsers haben
wir als 4 definiert, als Konstante.
Also liefert dieser Vergleich TRUE zurück.
Nun aber bevor THREAD1 die Anzahl der Benutzer erhöhen kann, erfolgt
eine Umschaltung.
Das Betriebssystem sagt, jetzt ist auf einmal THREAD2 dran und THREAD2
möchte ebenfalls noch einen Benutzer anmelden.
Ruft die LOGON-Methode auf.
Die LOGON-Methode macht wieder ihren Vergleich, ist die Anzahl der
angemeldeten Benutzer immer noch kleiner als die Anzahl der maximalen
Benutzer?
Und da der erste THREAD diese Anzahl noch nicht verändert hat, steht
da nach wie vor eine 3 drin.
Und da eine 4.
Und der Vergleich ergibt wieder TRUE.
Und deswegen kann THREAD2 die Anzahl der Benutzer um 1 erhöhen.
Also sind jetzt 4 Benutzer am System angemeldet.
Dann gibt die LOGON-Methode den Wert TRUE zurück und THREAD2 ist
fertig und THREAD1 kommt wieder an die Reihe.
Und THREAD1 hat sich gemerkt, ok, diese Überprüfung habe ich schon
durchgeführt, der hat TRUE zurückgegeben.
Das heißt, ich kann jetzt ohne weiteres noch einen Benutzer
hinzufügen, weil es waren ja weniger Benutzer als die maximale Anzahl
angemeldet.
Also erhöht auch THREAD1 die Anzahl der Benutzer um 1.
Und ich habe jetzt auf einmal 5 Benutzer am System angemeldet.
Und die Methode gibt TRUE zurück.
Sie sehen also, dadurch, dass der erste THREAD an dieser Stelle hier
unterbrochen werden konnte, zwischen der Abfrage und der Erhöhung der
Variablen war es möglich, dass mehr Benutzer als eigentlich erlaubt am
System angemeldet sein konnten.
Und der Grund war eben mangelnde Synchronisation.
Im nächsten Aufgabenteil sollten Sie das Programm besser machen, also
verhindern, dass der THREAD an dieser Stelle unterbrochen wird und
sich das Programm korrekt verhält.
Und zwar sollten Sie das durch Einführung eines
Synchronisationsobjekts bewerkstelligen.
Hier haben wir nochmal den ursprünglichen Code, wie wir auf dem
Übungsblatt drauf waren, wieder die Logon-Methode, um die es geht.
Und die können wir jetzt verhindern, dass die Logon-Methode hier an
dieser Stelle unterbrochen wird.
Führen wir ein Objekt LOG ein, das wir zur Synchronisation benutzen
wollen und kapseln den betreffenden Code in ein Synchronized
Statement.
Das bedeutet praktisch, wenn hier mit dieser Variable gesperrt ist,
dieser Code, dann kann er nicht mehr von einem anderen THREAD betreten
werden.
Das heißt, wenn ein THREAD hier reingeht und dieses LOG sperrt, dann
kann er zwar immer noch unterbrochen werden, aber derjenige, der ihn
unterbricht, kann diesen Bereich nicht mehr betreten.
Und das bedeutet wiederum, ich muss alle Bereiche, die irgendwas an
meinen NUM-Users ändern können, durch dieses LOG schützen, damit
niemand mehr, nachdem die Überprüfung stattgefunden hat, meine NUM
-Anzahl verändern kann.
Das heißt, ich muss auch noch in der LOGOFF-Methode dieses
Synchronized Statement einfügen, weil das sind die zwei Bereiche im
Programm, wo sich die NUM-Users-Variable ändert.
In der GET-Users-Methode brauche ich kein LOG einführen, weil da wird
die Variable ja nur gelesen, aber ändert ihren Wert nicht.
Ist jedem klar, warum jetzt das Programm korrekt funktioniert?
Die Einführung von LOG-Variablen ist aber nur eine Möglichkeit, wie
man Code vor Nebenläufigkeit schützen kann.
Es gibt noch eine andere Möglichkeit, das ist das sogenannte Monitor
-Konzept.
Dadurch die Synchronisation nicht für einzelne Code-Abschnitte,
sondern auf Methodenebenen durchführen.
Das heißt, ich kennzeichne ganze Methoden als synchronized und sage,
diese Methoden dürfen nur einmal maximal ausgeführt werden.
Das heißt, wenn ein Kontrollfaden eine Methode aufruft, die
synchronized ist, kann kein anderer Kontrollfaden eine Methode des
selben Objekts aufrufen, das synchronized ist.
Wie sieht das aus?
Hier haben wir das ursprüngliche Programm.
An der Stelle eines Objekts füge ich jetzt einfach die Synchronized
Statements direkt vor der Methodendeklaration ein.
Meine Methode LOGON soll synchronized sein und meine Methode LOGOF
auch.
Das heißt, wenn Thread1 jetzt hier die LOGON-Methode aufrufen würde
und befindet sich da irgendwo zwischendrin, kann Thread2 nicht auch
noch die LOGON-Methode aufrufen.
Und deswegen kann es zu solchen Fehlern nicht mehr kommen.
Und das ist die andere Möglichkeit.
Gut, im letzten Teil war die Frage, was ändert sich dran, wenn ich
statt int-Variablen Variablen vom Typ long verwende.
Das ist jetzt erstmal die Frage, warum ändert sich da überhaupt was?
Was hat das denn mit dem Datentyp zu tun, wann ich synchronisieren
muss?
Das Problem ist jetzt, dass Variablen vom Typ long 64-Bit haben.
Die meisten heutigen Prozessoren arbeiten aber nur mit 32-Bit-breiten
Wörtern.
Das heißt, wenn ein Prozessor mit 32-Bit eine 64-Bit-Variable lesen
oder kopieren will, muss er dies in zwei Schritten tun.
Das heißt, wenn man hier im Thread 1 den Inhalt von Variable A nach
Variable B kopieren will, dann erfolgt das in zwei Schritten.
Dann wird z.B.
erst die erste Hälfte übertragen und dann wird die zweite Hälfte
übertragen.
Und da kann es natürlich zu Problemen kommen, wenn genau mittendrin
dieser Thread 1 unterbrochen wird.
Und das wollen wir uns jetzt anschauen.
Also Thread 1 kopiert den ersten Teil von A nach B.
Das sind die Nullen.
Und dann sollte eigentlich der zweite Teil kopiert werden, aber jetzt
kommt Thread 2 und sagt, ich möchte jetzt aber A auf diesen Wert hier
setzen.
Und weist diesem Variable A den Wert zu.
Dann kommt wieder Thread 1 ran und setzt seinen Kopiervorgang fort.
Und kopiert den zweiten Teil, der jetzt den neuen Wert enthält.
Und auf einmal habe ich in B einen Wert stehen, den Variable A zu
keinem Zeitpunkt gehabt hat.
Das ist also vollkommen inkonsistent.
Denn das einzige was logisch wäre, dass entweder B diesen Wert hat
oder den Wert, den A vor der Änderung gehabt hat.
Aber nicht so ein Mischmasch.
Das heißt, ich muss durch Synchronisation verhindern, dass das Lesen
von A unterbrochen wird.
Welche Änderungen muss ich jetzt in meinem Programm durchführen?
Also wenn ich hier statt Int immer Long-Variablen habe, dann muss ich
nicht nur das Logon und das Logoff synchronisieren, sondern auch das
GetNumUsers, mache ich hier auch das Synchronized Statement.
Denn dieses Return NumUsers hier macht nichts anderes als den Wert von
NumUsers auszulesen.
Und wenn NumUsers eine Long-Variable ist, wird der Wert eben in zwei
Schritten gelesen.
Und ich muss dafür sorgen, dass während meines Lesens nicht durch
Logon oder Logoff der Wert verändert wird und ich so ein
inkonsistentes Ergebnis haben könnte.
Das war Aufgabe 3.
Gibt es dazu Fragen?
Gut, kommen wir zur letzten Aufgabe.
Da ging es um Verklemmungen oder Deadlocks.
Und im A-Teil sollten Sie einfach mal definieren, was ist überhaupt
ein Deadlock.
Da fangen wir mal an.
Was brauche ich dazu alles?
Zunächst brauche ich eine Menge von Threads.
Die haben wir hier.
Und dann ist ein Deadlock dadurch gekennzeichnet, dass nichts mehr
vorangeht.
Dass jeder irgendwie wartet und das System nicht weiterkommt.
Also jeder Thread wartet auf irgendein Ereignis und das wollen wir
jetzt mal kennzeichnen durch so ein Pfeil.
Nehmen wir mal an, Thread 1 wartet darauf, dass Thread 2 endlich den
kritischen Abschnitt verlässt.
Und Thread 2 könnte zum Beispiel auf eine Benutzereingabe warten.
Wäre das eine mögliche Deadlock-Situation, wenn ein Thread auf eine
Benutzereingabe wartet?
Ist da jemand dafür?
Dagegen?
Auch nicht?
Alle keine Meinung?
Na sowas.
Also warten auf eine Benutzereingabe ist keine Deadlock-Situation.
Denn Deadlock ist immer dadurch gekennzeichnet, dass sich daran nichts
ändert, dass das System praktisch ewig feststeckt.
Aber wenn ich auf eine Benutzereingabe warte, das ist ein Ereignis von
außen, das irgendwann eintreffen kann, dann kommt das System weiter,
sobald der Benutzer was eingibt.
Während in einem Deadlock das System ewig in diesem Zustand bleibt,
weil es in sich verklemmt ist und ich nicht eingreifen kann.
Also Benutzereingabe wieder weg.
Deadlock ist nur gegeben, wenn die Threads alle auf Ereignisse warten,
die nur von einem jeweils anderen Thread ausgelöst werden können.
Also Thread 2 wartet z.B.
auf Thread 3, Thread 3 wartet auf Thread 4, Thread 4 wartet auf Thread
5 und Thread 5 wartet wieder auf Thread 1.
Das wäre ein Deadlock, wenn die so im Kreis herum warten und jeder
wartet darauf, dass der andere was macht und keiner tut was und das
System steht.
Das ist ein Deadlock.
Und wenn ich das formaler definieren wollte, könnte ich sagen, ein
Deadlock bezeichnet den Zustand, in dem sich eine Menge von Threads
befindet.
Und jeder Thread auf das Eintreten eines Ereignisses wartet, welches
nur von einem anderen zu der Menge gehörenden Thread ausgelöst werden
kann.
Also das beschreibt genau das, was wir hier haben.
Bisher hatten wir als Beispiele für das Warten eigentlich immer, dass
ein Thread sich in einem kritischen Bereich befindet.
Z.B.
in einer Synchronize-Methode, wie wir es vorhin hatten.
Aber wenn man das allgemeiner sieht, kann man es sich vielleicht auch
besser vorstellen.
Das Warten könnte auch heißen, dass ein Prozess z.B.
darauf wartet, dass er etwas auf dem Bildschirm ausgeben kann, aber
ein anderer Thread gerade den Bildschirm belegt hält oder etwas auf
dem Drucker ausgibt.
Z.B.
hier hat der Prozess 1 den Bildschirm belegt und wartet auf den
Drucker, während der andere Prozess den Drucker belegt hat und auf den
Bildschirm wartet.
Und der eine sagt, ich gebe aber den Drucker nicht frei, bevor ich den
Bildschirm bekomme und der andere sagt, ich gebe den Bildschirm nicht
frei, bevor ich den Drucker nicht habe.
Und deswegen befindet sich das System im Deadlock und wartet ewig.
Der Benutzer empfindet es dann meistens als Absturz, also es tut sich
nichts mehr und ich weiß eigentlich nicht so richtig, was los ist.
Und ein möglicher Grund könnte z.B.
ein Deadlock sein.
Dann die nächste Frage ist, wie viele Kontrollfäden müssen mindestens
vorhanden sein, damit es überhaupt einen Deadlock geben kann.
Fangen wir mal an.
Ist es möglich, dass bei einem Thread ein Deadlock auftritt?
Ja oder nein?
Es schüttelt den Kopf.
Also es ist in der Tat so, wenn ein Thread auf sich selbst wartet,
dann bezeichnet man es nicht als Deadlock.
Das würde ja heißen, wenn man sich das mal überlegt, ein Thread wartet
darauf, dass er auf ein Ereignis, das nur er selbst auslösen kann.
Das ist irgendwie nicht ganz so logisch, weil wenn er dieses Ereignis
nur selbst auslösen kann, dann müsste er das ja entweder im Code
vorher oder nachher auslösen.
Und wenn das vorher schon ausgelöst hätte, würde er nicht warten, wenn
er es nur nachher auslösen kann.
Wie soll er jemals an diese Codestelle drankommen?
Also das kann höchstens eine Endlosschleife sein und das ist kein
Deadlock.
Und wenn wir nochmal in die Definition reinschauen, die heißt aus
diesem Grund auch, dass das Ereignis muss von einem anderen Thread aus
der Menge ausgelöst werden können.
Das heißt, es sind mindestens zwei Threads in einem Deadlock
bezeichnet.
Also hier, wenn ein Thread auf den anderen wartet und der andere auf
den einen, das ist ein echter Deadlock.
Und das kann natürlich eine beliebige Anzahl von Threads entfassen,
solange es mindestens zwei sind.
Gut, zur letzten Aufgabe.
Die geht wieder etwas mehr in die Praxis.
Da hat ein Entwickler eine tolle Anwendung entworfen und ist sich aber
nicht ganz sicher, ob da eventuell Deadlocks drin sein könnten.
Und denkt sich, naja, probiere ich es halt mal aus, ob es funktioniert
und dann werden wir schon weitersehen.
Also nehmen wir an, den einfachsten Fall.
Die Anwendung hat zwei Threads.
Und der Entwickler lässt die Anwendung laufen.
Der erste Thread möchte es auf dem Drucker ausdrucken.
Der zweite Thread liest Daten von der Platte ein.
Dann möchte der erste Thread noch eine Meldung auf dem Bildschirm
ausgeben.
Achtung, ich drucke jetzt.
Und irgendwann hat er dann fertig gedruckt, gibt den Drucker wieder
frei, gibt den Bildschirm wieder frei.
Und der zweite Thread hat die Platte dann fertig benutzt und möchte
seine Daten auf dem Bildschirm ausgeben.
Und auch noch auf den Drucker und hat den Bildschirm fertig, den
Drucker fertig gebraucht.
Und das Programm hat sich beendet und alles ist korrekt gelaufen und
der Entwickler denkt, wunderbar, hat funktioniert.
Zur Sicherheit probiert er das Ganze nochmal aus.
Wieder fängt der erste Thread an zu drucken.
Der zweite geht auf die Platte.
Der erste benutzt den Bildschirm, gibt den Drucker frei, den
Bildschirm frei.
Der zweite ist mit der Platte fertig, gibt auf den Bildschirm aus, auf
den Drucker und das Programm ist fertig.
Und der Entwickler ist glücklich und denkt, wunderbar.
Jetzt würden sie dem empfehlen, seine Anwendung so zu verkaufen.
Scheint ja alles in bester Ordnung zu sein.
Schauen wir uns das mal an.
Der Entwickler verkauft seine Anwendung, findet sich auch einen
Benutzer, der kauft die.
Allerdings hat er in seinem System erst vor kurzem die neue Super Mega
Disk 9000 mit der sensationellen niedrigen Zugriffszeit von 0,3
Millisekunden eingebaut.
Und denkt, jetzt kann überhaupt nichts mehr schief gehen, jetzt muss
meine Anwendung wie geschmiert laufen.
Und fängt also an, das Ding auszuführen.
Wieder fängt der erste Thread an zu drucken.
Der zweite holt sich seine Daten von der Platte und weil die Platte so
schnell ist, ist sie auch schon fertig.
Denkt wunderbar, dann kann ich ja gleich anfangen, die Daten
auszugeben auf dem Bildschirm.
Und der erste Thread kommt dran und will seine Meldung ausgeben und
ups, der Bildschirm ist ja schon belegt.
Na ja, warte ich eben, bis er wieder frei ist.
In der Zeit kommt der zweite Thread wieder dran und möchte drucken und
der Drucker ist auch irgendwie belegt.
Na ja, warten wir eben.
Und der zweite wartet auf den Drucker, der erste wartet auf den
Bildschirm und das Ganze geht ewig so weiter.
Das System tut sich nichts mehr.
Wir haben den wunderbarsten Deadlock.
Und der Benutzer freut sich dann natürlich auch wahnsinnig drüber und
denkt, schon wieder abgestürzt.
Also das Problem war eben, dass man durch Testen nie herausfinden
kann, ob im System wirklich Deadlocks enthalten sind.
Weil ob Deadlocks auftreten, hängt stark vom Zeitverhalten ab, also
wie lange brauchen meine einzelnen Zugriffe, wie lange sind meine
kritischen Abschnitte.
Und das hängt eben davon ab, wie schnell meine Hardware ist.
Wenn ich auf ein anderes System gehe, kann es sein, dass eine
Operation viel schneller oder viel langsamer beendet ist.
Und es kann alles gut gehen, wenn die Abschnitte in der richtigen
Reihenfolge dran kommen.
Aber wenn sich durch diese Geschwindigkeitsveränderung etwas ändert in
der Ausführungsreihenfolge, dann kann es auf einmal ordentlich schief
gehen.
Also durch Testen kann ich sowas nicht herausfinden.
Gut, dann wäre ich am Ende angelangt.
Und wenn Sie keine Fragen mehr haben, dann möchte ich Ihnen ein
schönes Wochenende und viel Erfolg bei der Klausur nächste Woche
wünschen.
Also nachlesen kann man es auf jeden Fall im IPO und die Verteilung
wird wann gemacht?
Also Sie können sich ja noch grundsätzlich anmelden bis zum Dienstag
und vorher kann ich die Verteilung natürlich noch nicht erstellen.
Das heißt ab dem Zeitpunkt werde ich es dann veröffentlichen, es wird
ausgehängt werden bei uns am Institut drüben im Infoneubau und wir
werden es auch ins IPO hängen.
Sodass Sie auch dort diese Informationen finden, da steht dann die
Matrikelnummer und der Hörsaal jeweils mit drin und daraus können Sie
dann ablesen, dass Sie a.) korrekt angemeldet sind und b.) in welchem
Hörsaal Sie sich befinden.
Falls es da ein Problem gibt, bitte dann melden.
Möglichst also vor dem Tag, wo die Klausur stattfindet, damit wir das
noch ändern können.
Okay?