Vorlesung Verteilte Systeme Übungsblatt 2 Aufgabe 1: Beschreiben Sie (z.B. mit Pseudocode) die zwei grundsätzlichen Möglichkeiten, wie man in Java Threads erzeugt. Aufgabe 2: Welche der folgenden Aussagen sind wahr oder falsch? a) Der schreibende Zugriff auf Objektattribute einer Klasse von verschiedenen Threads aus ist immer unkritisch. b) Der schreibende Zugriff auf lokale Variablen einer Klassenmethode von verschiedenen Threads aus ist immer unkritisch. c) Der schreibende Zugriff auf Klassenattribute (static-Attribute einer Klasse) von verschiedenen Threads aus ist immer kritisch. d) Wenn ein Thread in einem wait() auf etwas wartet, kann dieser Wartezustand nicht durch Schicken eines „Interrupts“ (per Aufruf der interrupt()-Methode) unterbrochen werden. e) Der Aufruf von notify() auf einem Datenobjekt, an dem Threads warten, weckt alle wartenden Threads auf. f) Wenn nur ein Thread schreibend auf eine Resource zugreift und die anderen nur lesen, dann muss der Zugriff nicht synchronisiert werden. g) Der Aufruf der Methode interrupt() auf einem Threadobjekt wirft eine InterruptedException, wenn dieser Thread auf etwas wartet (z.B. in einem wait()Aufruf). h) Der Aufruf der Methode interrupt() auf einem Threadobjekt wirft auch dann eine Interrupted-Exception, wenn der Thread nicht auf etwas wartet. i) Wenn zwei Threads nacheinander auf verschiedene Objekte als gemeinsame Ressourcen schreibend zugreifen, dann sollten die Objekte von beiden Threads immer in der gleichen Reihenfolge gesperrt werden, um Deadlocks zu vermeiden. j) Eine Java-Klasse hat zwei mit synchronized verriegelte Methoden m1() und m2(). Man sollte nie Methode m1() in der Methode m2() oder umgekehrt aufrufen, sonst erhält man einen Deadlock. Aufgabe 3: Warum sollte man in Java und auch in anderen Sprachen einen Thread nie durch Aufruf einer Art stop()-Methode von einem anderen Thread aus unmittelbar beenden können, bzw. warum ist diese Methode in Java „deprecated“. Wie sollte man dann Threads beenden? Aufgabe 4: (Programmieraufgabe) Erläutern Sie am Beispiel (z.B. das Threadbeispiel aus der Vorlesung), was Nichtdeterminismus einer Verteilten Anwendung ist. Aufgabe 5: (Programmieraufgabe) Schreiben Sie den Serverteil der Wetteranwendung so um, dass er einen Threadpool von Threads zur Verarbeitung der empfangenen Datenpakete auf der Serverseite verwendet. Verwenden sie hierfür den ExecutorService des java.util.concurrent-Paketes. Vorlesung Verteilte Systeme Musterlösungen zu Übungsblatt 2 Aufgabe 1: Lösungsvariante 1 ist, eine eigene Klasse von der Klasse Thread abzuleiten und dann zu instanziieren. public class MyThread extends Thread { public void run() { // do something } } MyThread thread = new MyThread(); thread.start(); Die zweite Lösung ist, eine Klasse zu erstellen, die das Interface Runnable implementiert, und dann ein Thread-Objekt mit einem Objekt dieser Klasse als Argument im Konstruktor zu erzeugen. public calls MyRunnable implements Runnable { public void run() { // something to run } } Thread thread = new Thread(new MyRunnable()); thread.start(); Aufgabe 2: a) Falsch (wenn mehrere Threads auf das gleiche Objekt zugreifen, muss der Zugriff auf Objektattribute verriegelt werden) b) Wahr c) Wahr (Hier greifen all eThreads bei Benutzung auf die selben Attribute zu) d) Falsch (interrupt() unterbricht gerade den Schlafzustand bei wait()) e) Falsch (Es wird nur ein einzelner Thread aufgeweckt; sonst notifyAll() verwenden) f) Falsch (auch bei nur einem Writer muss man verriegeln) g) Wahr h) Falsch (Die Exception wird nur geworfen, wenn sich der Thread in einem Wartezustand befindet, siehe (g)) i) Wahr j) Falsch (Ein Objekt blockiert sich nie selbst an einem von ihm gehaltenen Lock) Aufgabe 3: Da ein Master- oder Kontroll-Thread nicht wissen kann, was seine Worker-Threads gerade tun, sollte man diese auch nicht einfach mit einer Art stop()-Methode beenden können, ohne dass sie Zeit haben, ihre augenblickliche Arbeit in einen für das Programm sicheren Status zu überführen (z.B. geöffnete Ressourcen aufzuräumen oder eine Operation zum Eintragen von Daten in eine Datenbank auch vollständig zu beenden). Dies wird typischer Weise durch ein Benachrichtigungskonzept (Signalkonzept) erreicht, bei dem den zu beendenden Threads eine Nachricht (Signal) übermittelt wird, damit sich diese nach Empfang der Nachricht selbst beenden können (nachdem sie ihren Zustand in einen geeigneten Zustand gebracht haben). Hierfür dienen in Java die Methoden interrupt() (Senden der Nachricht) sowie die InterrruptedException und isInterrupted() Methoden (Empfang der Nachricht). Aufgabe 4: Im Beispiel der Vorlesung für Threaderzeugung haben die in der Main-Methode erzeugten Threads in einer Schleife über den Aufruf von System.out.println() Ausgaben auf der Konsole gemacht. Wenn man das Programm verschiedene Male startet, sieht die Reihenfolge der Ausgabe dabei immer anders aus. Sie scheint einem Betrachter von außen als zufällig, da sie nicht vom Programm sondern anderen Faktoren, wie das Scheduling des Betriebssytems oder der Java-Virtualmaschine abhängt, die für den Betrachter nicht nachvollziehbar sind. Dieses scheinbar-willkürliche Ineinanderverchachtelung der Ausgaben nennt man daher nicht-deterministisches (Nicht-vorhersehbares) Verhalten der Anwendung. Aufgabe 5: (10 Pkte) Das Programmbeispiel (siehe Sourcecode auf der Website) wurde mit Hilfe der ExecutorService-Klasse des java.util.concurrent-Packages unter Verwendung eines WorkerThreadPools mit fester Größe (Methode newFixedThreadPool()) auf der Serverseite parallelisiert. Der Code zum Eintragen der Daten in die Datenbank wurde dabei in eine Klasse DataProcessor ausgelagert, die das Runnable-Interface implementiert, in der die Arbeit zum Eintragen der Daten in die Datenbank gebündelt sind. Da jetzt von parallelen Threads auf die Datenbank zugegriffen wird, darf man nicht mehr mit einem gemeinsam genutzten Connection-Objekt arbeiten, da die Klasse Connection nicht Thread-Safe ist. Stattdessen wird ein Connection-Pool (auf Basis des c3p0-Frameworks) verwendet, der den Threads bei Bedarf eine Connection explizit für diesen Thread bereitstellt. Diese exklusive Connection wird dann mit Aufruf der close()-Methode an den Connection-Pool zurückgegeben, wenn sie nicht mehr gebraucht wird. Das Programm enthält auch Code, der den Thread-Pool terminiert und den Datagram-Socket schließt, wenn das Serverprogramm von der Konsole aus beendet wird.