Java Threads Mutex, Semaphor, Race Condition, Deadlock, Starvation und Co.. Prof. Dr. Nikolaus Wulff Java Threads • Threads sind „leichtgewichtige Prozesse“. Sie benötigen keine Betriebsystemressourcen wie ein echter Prozess. Die OS-Ressourcen hält die JVM... • Jeder Thread durchläuft seriell seinen Programmcode. • Ein Thread kann blockieren (z.B. System.in.read()) oder aber auch gezielt „schlafen“ gelegt werden: – z.B. this.wait(1000) oder Thread.sleep(1000) • Ein Thread kann einen anderen wieder aufwecken: – this.notify() oder this.notifyAll() • Damit dies funktioniert, muss der Thread in einer synchronisierten „Critical Section“ laufen. © Prof. Dr. Nikolaus Wulff Informatik III 2 Zeitlicher Ablauf von Threads • Auf Einprozessormaschinen laufen die Threads scheinbar parallel, auf Multicoreprozessoren real. • I.A. wird es immer mehr Prozesse & Threads als Prozessoren geben. • Jeder Thread erhält ein Zeitfenster zum Laufen. Thread A B C D © Prof. Dr. Nikolaus Wulff gerade aktiv Informatik III 3 Multitasking Paradigma • Mehrere Programme (Prozesse) laufen unabhängig voneinander, können jedoch mit einander kommunizieren. – Z.B. im Dateimanager wird per Drag & Drop eine Datei auf Notepad/Acrobat Reader etc. abgelegt und dort geöffnet... BetriebSystem Shared Memory © Prof. Dr. Nikolaus Wulff Anwendung Anwendung#1 #1 Lokaler Speicher Anwendung Anwendung#2 #2 Lokaler Speicher Anwendung Anwendung#3 #3 Lokaler Speicher Informatik III 4 Multithreading Paradigma • Eine der Anwendungen ist die JVM, die ihrerseits mehrere Threads verwaltet und synchronisiert. BetriebSystem Anwendung Anwendung#1 #1 Anwendung Java JavaJVM JVM#1 Lokaler Speicher JVM Anwendung Thread Thread#1 #1#1 Lokale LokalerVariablen Speicher Globale Variablen © Prof. Dr. Nikolaus Wulff Lokaler Speicher Thread Thread#2 #2 Lokale Variablen Thread Thread#3 #3 Lokale Variablen Informatik III 5 Eigene Threads • Java Threads sind Instanzen der Klasse Thread. • Diese werden mit der „start“ Methode gestartet, ab dann wird vom Thread dessen „run“-Methode ausgeführt, die entsprechend überladen werden muss. • Alternative kann ein Thread mit einem „Runnable“ Objekt parametrisiert werden, dessen „run“-Methode dann ausgeführt wird. • Der Thread „lebt“ solange bis die run Methode beendet ist, entweder wg. einer Exception oder des natürlichen Endes. © Prof. Dr. Nikolaus Wulff Informatik III 6 Thread Verwendung • Zwei Möglichkeiten einen Thread zu verwenden: Durch Vererbung oder durch Parametrisierung mit einem Runnable Objekt. • In beiden Fällen muss eine geeignete run-Methode implementiert werden. © Prof. Dr. Nikolaus Wulff Informatik III 7 Thread Life-Cycle • Threads werden in Java als Instanzen der Klasse java.lang.Thread implementiert. • Der Thread Lebenszyklus wird geregelt durch die Methoden: – start: Beginn der asynchronen Ausführung, diese Methode startet die run Methode der jeweiligen Thread Instanz. – run: Die eigentliche Aufgabe des Thread wird in dieser Methode geleistet. Sobald diese Methode beendet wird, entweder durch return oder Exception ist der Thread „tot“. – stop: Beendet die run Methode und stoppt den Thread. Diese Methode soll nicht mehr verwendet werden, da sie nicht thread-safe ist... © Prof. Dr. Nikolaus Wulff Informatik III 8 Threads als FSM • Threads können (scheinbar) gleichzeitig parallel auf einem Prozessor laufen. • Hierbei hat jeder Thread seinen eigenen Stack für lokale Variablen und muss sich den Code und Datenbereich der JVM mit allen anderen Threads teilen. • Java Threads werden als Zustandsautomat modelliert. • Es gibt die fünf wichtigen Zustände CREATED, TERMINATED, RUNING, RUNABLE und NONERUNABLE, die durch geeignete Statusübergänge mit Hilfe von Thread Methoden erreicht werden können. © Prof. Dr. Nikolaus Wulff Informatik III 9 Thread Statusdiagramm Aktiv Schlafend Tot • Statusübergänge werden durch entsprechende Methoden der Thread Klasse ausgelöst. © Prof. Dr. Nikolaus Wulff Informatik III 10 Threadsteuerung • Methoden zum Steuern von Threads sind in den Klassen Objekt und Thread zu finden: • Object: wait, notify, notifyAll • Thread: sleep, interrupt, join, yield, destroy, stop, suspend, resume • Einige Methoden sind „Deprecated“ und sollen nicht mehr verwendet werden. D.h. einige der Thread-Stati müssen ohne obige Methoden erreicht werden! Infos hierzu liefert die JDK Dokumentation unter: Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated? © Prof. Dr. Nikolaus Wulff Informatik III 11 Ressourcen • Programme verwenden Ressourcen, diese lassen sich grob in drei Kategorien unterteilen: – sharable / teilbar – consumable / verbrauchbar – serially resusable / exklusiv wiederverwendbar • In diesem Kontext ist die letzte Gruppe interessant. Nur ein Teilnehmer ( ≡ Prozess oder Thread) zur Zeit darf diese verwenden. • Typische Beispiele sind: Datenbank- und Socketverbindungen, der Drucker, die Festplatte,... © Prof. Dr. Nikolaus Wulff Informatik III 12 Problematisches Teilen • Race Condition / Inferenz – Verschiedene Teilnehmer manipulieren quasi gleichzeitig, bzw. zeitlich überlappend, die selbe Ressource. Um dies zu verhindern werden Sperrmechanismen, wie Critical Section, Mutex und Semaphore, eingeführt. • Deadlock / Verklemmung – Die Sperren können dazu führen, das sich zwei Teilnehmer derart blockieren, das keiner von beiden weiter ausgeführt werden können... • Life Lock / Starvation / Verhungern – Massnahmen gegen Deadlock führen bei ungerechter Verteilung dazu, dass einige Teilnehmer nie die Ressource erhalten... © Prof. Dr. Nikolaus Wulff Informatik III 13 Vier Bedingungen zur Verklemmung • Vier Bedingungen sind notwendig, damit es zu einer Verklemmung kommt: (1) Wechselseitiger Ausschluss: Nur ein Teilnehmer zur Zeit hat exklusiven Zugriff auf Ressource A. (2) Halten und Warten: Der Teilnehmer hält und blockiert Ressource A während er auf Ressource B wartet. (3) Keine Verdrängung: Der Teilnehmer kann nur selbst die Ressource A wieder freigeben, ist er nicht gutmütig gibt es keine Chance ihm diese zu entziehen (4) Zyklische Wartesituation: Die Teilnehmer können in eine zyklische Abhängigkeitssituation kommen. © Prof. Dr. Nikolaus Wulff Informatik III 14 Vier Strategien gegen Verklemmung • Vorbeugen – Durch Einführen einer Hierarchie und Vorgabe diese immer in gleicher Reihenfolge anzufordern wird (4) durchbrochen. • Vermeiden – Einem Teilnehmer wird verboten eine Ressource zu belegen, nur um anschließend zu merken das noch eine weitere fehlt. Dadurch wird (2) durchbrochen. • Entdecken – Analyse ob es zur Situation (4) kommen kann. • Auflösen – Kommt es zu einer Verklemmung wird die Ressource wieder entzogen und somit (3) durchbrochen. © Prof. Dr. Nikolaus Wulff Informatik III 15 Teilnehmer-Ressourcen Graph • Teilnehmer-Ressourcen Abhängigkeiten lassen sich als gerichteter Graph präsentieren: T1 T2 R1 R2 T hält R T benötigt R • Graph einer zyklischen Abhängigkeit: T1 hält R1 und wartet auf R2 während T2 auf R1 wartet und R2 hält. © Prof. Dr. Nikolaus Wulff Informatik III 16 Wait-For Graph • Abstraktion der Ressourcen zeigt die reine Abhängigkeit der Teilnehmer voneinander: T1 T2 Tj wartet auf Tk • An Hand eines solchen WFG Graphen lassen sich zyklische Abhängigkeiten identifizieren. © Prof. Dr. Nikolaus Wulff Informatik III 17 Identifizierung von Zyklen • Es gibt verschiedene Möglichkeiten einen Zyklus in einem Wait-For Graphen zu entdecken: 1) Auswertung der Adjazenzmatrix AG und deren Transitiven Hülle RG. Gibt es in der Diagonalen von RG Einsen, so liegt eine Verklemmung vor. 2) Rekursive Reduktion des Wait-For Graphen. Bleiben nach der Reduktion noch Teilnehmer T über so liegt eine Verklemmung vor. © Prof. Dr. Nikolaus Wulff Informatik III 18 Warshall Algorithmus • Die Bestimmung von RG = ∪ Bk erfordert eine aufwendige Berechnung aller Potenzen von AG. • Einfacher ist der Algorithmus nach Warshall (1962): for(k=0; k<N; k++) agrk agkc for(r=0; r<N; r++) if( ag[r][k]==true) { agrc for(c=0; c<N; c++) ag[r][c] |= ag[k][c]; } • Die Elemente agrc der Matrix AG werden als boolsche Variablen mit Werten {false,true} betrachtet. © Prof. Dr. Nikolaus Wulff Informatik III 19 Rekursive Reduktion des WFG • Der folgende Pseudocode eliminiert rekursiv alle Teilnehmer Tk die ordnungsgemäß terminieren, d.h. keine Abhängigkeiten haben und somit ihre Ressourcen freigeben. for(; ;) { find node Tk with no outgoing edges; if( no such node ) break; erase Tk and all incoming edges; } if (any Tk are left) there is a deadlock; © Prof. Dr. Nikolaus Wulff Informatik III 20 Java Thread Synchronisierung • Shareable Ressourcen müssen über Thread-Grenzen hinweg synchronisiert werden. • In der Java Sprache sind hierzu das synchronizied Schlüsselwort sowie die zugehörigen Methoden wait, notify und notifyAll zur Inter-Thread-Kommunikation in der Klasse Objekt vorgesehen. • Weitere Möglichkeiten sind seit dem JDK 1.5+ in den neuen Paketen java.util.concurrent.* zu finden: – Callable<V> Runnable mit Ergebnis vom Typ V. – Future<V> Resultat einer asynchronen Berechnung. – sowie zahlreiche Implementierungen und Hilfsklassen... © Prof. Dr. Nikolaus Wulff Informatik III 21 Java Synchonisierungsmechanismen • Jedes Java Objekt besitzt ein zugehöriges Monitor Objekt, das von der JVM als Sperre verwendet wird. • Die Sperre wird per synchronized Schlüsselwort an dem Objekt gesetzt. Nur ein Thread zur Zeit kann eine solche Critical Section exklusiv durchlaufen. public synchronized void foo() { // begin critical section doSomething_Critical(); } // end critical section • Intern wird im Bytecode der JVM ein monitorenter und monitorexit Opcode erzeugt. © Prof. Dr. Nikolaus Wulff Informatik III 22 Java synchronized Block • Die Sperre ist immer mit dem zugehörigen Java Objekt verbunden. • Dies ist bei einer Methode das „this“-Objekt. • Oder in einem synchronized Block das explizit angegebene Objekt (this) oder eine Instanz: public void bar() { doSomething_noneCritical(); // begin critcal section block synchronized(anObject) { doSomething_Critical(); } // end critical section block doMore_noneCritical(); } © Prof. Dr. Nikolaus Wulff Informatik III 23 synchronized und Deadlocks • Durch synchronized Methoden & Blöcke werden Race Conditions eleminiert. • Falls jedoch direkt oder indirekt wait gerufen wird, z.B. per RessourcePool besteht Deadlock Gefahr. • Als Beispiel das N-Philosophen Problem: • N Philosophen sitzen an einem Tisch und denken oder aber essen. Zum Essen benötigen sie eine Gabel und einen Löffel. Es gibt aber nur M<N Gedecke. • Die zwei Ressourcenarten Gabel und Löffel werden jeweils durch einen ResourcePool<T> synchronisiert. Dadurch kann es zu einem Deadlock kommen. (Wie?) © Prof. Dr. Nikolaus Wulff Informatik III 24 Sequenz der Philosopheninteraktion • Jeder Philosoph wird als eigenständiger Thread gestartet, so dass alle unabhängig laufen. • Um zu Essen müssen nacheinander zwei Ressourcen Gabel und Löffel -, vom Tisch genommen werden. • Erhält der Philosoph keine von beiden so wartet er. • Erhält er nur eine Ressource, so blockiert er diese und wartet auf die Zweite. • Erst wenn er beide Ressourcen hat isst er und legt anschließend Gabel und Löffel auf den Tisch zurück, um wieder zu denken. • Der nächste blockierte Philosoph wird geweckt... © Prof. Dr. Nikolaus Wulff Informatik III 25 Philosophen Runde Forks und Spoons werden durch zwei Semaphore synchronisiert. • N Philosophen wetteifern um die M Ressourcen. – Ohne Synchronisierung kommt es zur Race Condition. – Mit (falscher) Synchronisierung zum Deadlock. © Prof. Dr. Nikolaus Wulff Informatik III 26 Philosophen Deadlock • Bei zufälliger Reihenfolge von Messer/Löffel-Zugriff kommt es nach einiger Zeit zur Verklemmung. Solche Fehler sind sehr schwer durch Testen zu entdecken! © Prof. Dr. Nikolaus Wulff Informatik III 27 Semaphore & ResourceManger • Eine Implementierung des ResourcePools wird synchronisierte Methoden verwenden, um die Resourcen zu schützen. (Siehe Übung). public interface ResourcePool<T> { /** * Acquire a resource of type T, * this call might block if none available. * @return T resource */ T acquire(); /** * Release a previously acquired resource back to the pool. * @param o T resource to release. */ void release(final T o); } © Prof. Dr. Nikolaus Wulff Informatik III 28 Semaphore & wait-notify • Synchronisierung lässt sich per wait-notify oder gekapselt per Semaphore erreichen. volatile int size; public synchronized void release(T resource) { size++; pool.add(resource); notify(); } public synchronized T require() { try { public void release(T resource) { while (size == 0) { pool.add(resource); wait(); semaphore.release(); } } } catch (InterruptedException e) { e.printStackTrace(); public synchronized T require() { } try { size--; semaphore.acquire(); return pool.remove(0); } catch (InterruptedException ex) { } ex.printStackTrace(); } return pool.remove(0); } © Prof. Dr. Nikolaus Wulff Informatik III 29 Achtung • Ein Semaphor eliminiert Race-Condition. Der Pool wird einen Client solange blockieren, bis eine freie Ressource zur Verfügung steht. Dies generiert leider weitere Probleme: • Wer garantiert, dass der Client die Ressource in den Pool zurückgibt? • Wer verhindert, dass nach der Rückgabe an den Pool diese Ressource nicht weiterhin vom ursprünglichen Client verwendet wird? © Prof. Dr. Nikolaus Wulff Informatik III 30 Abhilfe Dies lässt sich mit Hilfe verschiedener Strategien verhindern: • Die Referenz wird ungültig nach dem release-Aufruf. • Die Referenz ist eine bestimmte Zeitspanne gültig. Danach generiert ihrer Verwendung durch den Client eine TimeoutException. • Ein interner GarbageCollector sammelt nicht zurückgegebene Ressourcen ein und gibt diese in den Pool zurück. Die Referenz ist ab dann ungültig. © Prof. Dr. Nikolaus Wulff Informatik III 31 Problem • Wenn die Referenz entweder ungültig wird oder einen Timeout hervorruft lohnt es sich nicht diese in einem Pool zu cachen... Lösung: • Der Pool gibt keine „echte Referenz“ an den Client heraus, sondern ein Stellvertreterobjekt, ein Proxy. • Dieses Proxy hat dieselbe Schnittstelle wie die echte Referenz, kann aber durch den Pool administriert werden, um invalide oder per timeout disabled oder garbage-collected zu werden. © Prof. Dr. Nikolaus Wulff Informatik III 32 ResourcePool Proxy © Prof. Dr. Nikolaus Wulff Informatik III 33 Prinzip des Proxy • Ressource und Proxy implementieren die selbe Schnittstelle <T>. • Jeder Methodenaufruf wird vom Proxy an die Reference weiter delegiert: public int foo() { return getReference().foo(); } • Die getReference Method bietet die Möglichkeit zu überprüfen, ob die Reference noch gültig ist! public T getReference() { checkAccess(); return super.getReference(); } © Prof. Dr. Nikolaus Wulff Informatik III 34 Resource Recycling • Der ResourcePool invalidiert ein Proxy wenn es per release zurückgegeben wird. Ab dann ist die Ressource mit diesem Proxy nicht mehr erreichbar, da jeder Aufruf zu einer Exception beim Client führt. • Sollte das Proxy nicht zurückgegeben werden, so hat dies einen Timestamp für seine Lebensdauer, danach führt jede weitere Verwendung zu einer Exception. • Der ResourcePool hat einen Garbage-Collector Thread, der alle herausgegebenen Proxys nach einer vorgegebener Zeit invalidiert und die Resourcen in den Pool zurückgibt. So werden sie dem Client effektiv entzogen falls er den release-Aufruf vergißt... © Prof. Dr. Nikolaus Wulff Informatik III 35 Timeout • Überprüfung der Lebensdauer: /** * check that the access is within the * time frame, otherwise an exception * will be thrown. */ private void checkAccessTime() { long now = System.currentTimeMillis(); if (now>endTime) { String msg = String.format("time excess: %d mSec delay", now-endTime); throw new TimerException(msg); } } • Wird dies in der getReference() Methode aufgerufen, so ist das Proxy nach dem Timeout für den Client effektiv nicht mehr zu gebrauchen... © Prof. Dr. Nikolaus Wulff Informatik III 36 Proxy Vor- und Nachteile • Mit Hilfe des Proxy Ansatz lassen sich Resourcen effektiv und sicher wieder an den ResourcePool zurückgeben. • Ein Nachteil ist, dass hierzu eine gemeinsame Schnittstelle vorliegen muss, oder die Proxys müssen von der Ressource ableitbar sein. (Dann sind es keine eigentlichen Proxys mehr, sondern Dekorierer...) • C++ gestattet es die Referenzierungsoperatoren „->“ und „*“ sowie den Zuweisungsoperator „=“ zu überladen, so dass mit einem „intelligenten Zeiger“ eine effektive Überprüfung der Zugriffe erfolgt. © Prof. Dr. Nikolaus Wulff Informatik III 37 C++ Handle template <class T> class Handle { private: T* ptr; public: Handle() :ptr(0) { } Handle(T* ref) :ptr(ref) { assert(ptr != 0); } ~Handle() { if(ptr) delete ptr; ptr=0;} // assignment Handle& operator=(const Handle&); // pointer access T& operator*() const; T* operator->() const; operator bool() const {return ptr!=0;} // invalidate the handle void invalidate() { ptr = 0;} }; © Prof. Dr. Nikolaus Wulff Informatik III 38 Daumenregeln gegen Deadlocks • Lokale Methodenvariablen sind immer Thread-sicher und benötigen keine Synchronisierungsmechanismen. • Globale read-only Variablen sind Thread-sicher – also entweder final primitive Attribute – oder Objekte, die selber wiederum nicht veränderbar sind. • Synchronisierte Methoden vermeiden oder aber sicherstellen, das diese immer! in der selben Reihenfolge Lock-Objekte alloziieren. – letzte Bedingung erfordert Analyse des WFG. – => UML Sequenzdiagramm kann helfen © Prof. Dr. Nikolaus Wulff Informatik III 39 Literatur • „Java Threads“, S. Oaks und H. Wong, O'Reilly, 1997 – Das gesamte Buch handelt von Konzepten zur Thread Synchronisierung. • „Threads und Netzwerk-Programmierung mit Java“, H. Kredel und A. Yoshida, dpunkt, 1999 – Konzepte zur Parallelverarbeitung für Netzwerk- und Thread-Programmierung. • „Inside the Virtual Machine“, B. Venners, McGrawHill, 1998. – Kapitel 20 erläutert die Critical Section innerhalb der JVM • „Die Programmiersprache Java“, K. Arnold und J. Gosling, Addison-Wesley, 1996 © Prof. Dr. Nikolaus Wulff Informatik III 40