6. Kapitel MULTITHREADING Techniken der Programmentwicklung Prof. Dr. Wolfgang Schramm Übersicht 1 1. Programmstrukturierung mit Paketen (packages) 2. Vererbung 3. Abstrakte Klassen und Interfaces 4. Ausnahmebehandlung 5. Datenströme: die Java IO-­‐Bibliothek 6. MulUthreading 7. Innere Klassen 8. CollecUons 9. Generics 10. ReflecUon Lernziele des Kapitels 2 2 ¨ ¨ ¨ ¨ ¨ ¨ ¨ ¨ Verstehen, was Nebenläufigkeit ist und wie sie funkUoniert und in welchen Anwendungen sie eingesetzt wird. Die beiden wichUgsten Konzepte der Nebenläufigkeit kennenlernen. ImplemenUerung der Nebenläufigkeit in Java kennenlernen. Zustände von Threads kennenlernen. SynchronisaUonsmechanismen für Threads kennenlernen. Probleme bei der SynchronisaUon von Threads erkennen und vermeiden lernen. Eigene Threads implemenUeren und synchronisieren können. Threads verwalten können. Literaturempfehlung 3 ¨ Nicht nur für dieses Kapitel -­‐ auch darüber hinaus. Inhalt 4 o o o o o o o Nebenläufigkeit/MulUthreading: Grundlagen, Begriffe, Einsatz Prozesse vs. Threads Die Klasse Thread Das Interface Runnable SynchronisaUon Verwalten von Threads Das Paket java.uUl.concurrent MulUthreading -­‐ Grundlagen 5 o o o o o Nebenläufigkeit: Fähigkeit eines Systems, zwei oder mehr Vorgänge gleichzeiUg oder quasi-­‐gleichzeiUg ausführen zu können. ADA: erste Programmiersprache mit die direkt in die Sprache eingebejeten parallelen Prozessen und Mechanismen zur KommunikaUon und SynchronisaUon. KonvenJonelle Programmiersprachen: Realisierung von Nebenläufigkeit auf der Basis von Library-­‐RouJnen. Java: Nebenläufigkeit ist direkt in die Sprache integriert, mit den erforderlichen Hilfsmijeln zum Management der Nebenläufigkeit. Thread ¤ ¤ Eigenständiges Programmfragment, das parallel zu anderen Threads laufen kann. Ähnelt einem Prozess, arbeitet aber auf einer feineren Ebene. Wann ist parallele Programmierung sinnvoll? 6 Ressource Belastung Hauptspeicherzugriffe Prozessor Dateioperationen Festplatte Datenbankzugriffe Server, Netzwerkverbindung Prozesse vs. Threads 7 o Prozessmodell basiert auf 2 unabhängigen Konzepten: ¤ ¤ o Bündelung der Ressourcen Ausführung eines Programms Trennung der beiden Konzepte: ¤ Prozesse: Getrennte Adressräume (getrennter Heap) mit Quelltext und Daten. n Weitere Ressourcen: geöffnete Dateien, Sockets, Speicher, etc. n KommunikaUon über sog. InterprozesskommunikaUon (IPC). _ Prozesse werden benutzt, um Ressourcen zusammenzufassen. n ¤ Threads (Ausführungsfäden): n n n _ Getrennter Stack. KommunikaUon über den gemeinsamen Speicher (Heap). Threads laufen innerhalb eines Prozesses, haben weniger VerwaltungsinformaUonen als Prozesse (sind leichtgewichUger). Threads sind Einheiten, die für die Ausführung auf der CPU verwaltet werden. InterprozesskommunikaUon 8 Betriebssystem shared memory Prozess 1 Speicherbereich 1 Prozess 2 Speicherbereich 2 Prozess n Speicherbereich 3 KommunikaUon zwischen Threads 9 Prozess 1 Speicherbereich 1 Prozess 2 Speicherbereich 2 Betriebssystem Prozess 3 JVM Heap Thread 1 Stack 1 Thread 2 Stack 2 Thread 3 Stack 3 Prozess und Threads 10 Prozess Thread 1 o Adressraum • Befehlszähler o Globale Variablen • Register o Geöffnete Dateien • Stack o Kindprozesse o Zu behandelnde Signale • Zustand o Signale und SignalrouUnen o Threads … Thread n • Befehlszähler • Register • Stack • Zustand Prozesse / Threads: Einsatz 11 o o o o Prozess = Instrument zur Ausführung eines komplejen Programms. Innerhalb eines Prozesses können mehrere Threads parallel laufen. Der Laufzeit-­‐Overhead zur Erzeugung und Verwaltung eines Threads ist (weil leichtgewichUg) relaUv gering und kann in den meisten Programmen vernachlässigt werden. Einsatzbereich von Threads: ¤ ¤ Threads erleichtern unter anderem die ImplemenUerung grafischer Anwendungen, die durch SimulaUonen komplexer Abläufe oq inhärent nebenläufig sind. Threads können auch dazu verwendet werden, die Bedienbarkeit von Dialoganwendungen zu verbessern, indem rechenintensive Anwendungen im Hintergrund ablaufen. Threads in Java 1/2 12 o o o Durch die Klasse Thread und das Interface Runnable. Der Thread-­‐Body ( = parallel auszuführender Code) wird in Form der überschriebenen Methode run zur Verfügung gestellt. KommunikaJon der Threads untereinander: ¤ ¤ o durch Zugriff auf die Instanz-­‐ oder Klassenvariablen oder durch Aufruf beliebiger Methoden, die innerhalb von run sichtbar sind. SynchronisaJon: mijels Monitoren (s. Betriebssystemtheorie) à KoordinaUon des Zugriffs auf sog. kriUsche Abschnije (gemeinsam benutzte Daten). Threads in Java 2/2 13 o FunkUonen zur Verwaltung von Threads: ¤ ¤ ¤ o Zusammenzufassen von Threads in Gruppen, Priorisieren von Threads InformaUonen über Eigenschaqen von Threads beschaffen. Das Scheduling kann wahlweise unterbrechend oder nichtunterbrechend implemenUert sein. Die SprachspezifikaUon legt dies nicht endgülUg fest, aber in den meisten Java-­‐ImplemenUerungen wird dies von den Möglichkeiten des darunter liegenden Betriebssystems abhängen. Die Klasse Thread 14 o o o o Die Klasse Thread ist Bestandteil des Pakets java.lang Thread stellt die Basismethoden zur Erzeugung, Kontrolle und zum Beenden von Threads zur Verfügung. DeklaraUon einer Thread-­‐Klasse: Ableitung einer eigenen Klasse aus Thread und Überschreiben der Methode run. Thread ausführen: Aufrufs der Methode start à starten des Thread und Übertragung der weiteren Ausführung an die Methode run. Der Aufrufer kann parallel zum neu erzeugten Thread forrahren. public class Thread { Auszug public void start () { ... } public static void sleep (int millSeconds) { ... } public void run () { ... } ... } Threads – einfaches Beispiel 15 class MyThread extends Thread Run-Methode der Klasse MyThread public void run() { int i = 0; while (true) { System.out.println(i++); } } } public class ThreadTest { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); } } Starten des Thread t der Klasse MyThread Terminierung von Threads 16 o o Beenden eines Thread dadurch, dass das Ende seiner run-­‐ Methode erreicht ist. In manchen Fällen ist es jedoch erforderlich, den Thread von außen abzubrechen: ¤ ¤ durch Aufruf der Methode stop der Klasse Thread. durch ReakUon auf Unterbrechungsanforderungen im Thread selbst: n n o Speichern und Abfragen einer Unterbrechungsanforderung in eigenen Membervariablen. Verwendung von Methoden (interrupt, etc.) der Klasse Thread. Was ist bei einem Abbrechen von außen zu beachten? Terminierung eines Thread mit interrupt 17 package threads; public class InterruptTest extends Thread { private int cnt = 0; public void run() { while (true) { if (isInterrupted()) { break; } printLine(++cnt); // start with cnt = 1 } } public static void main(String[] args) { InterruptTest th = new InterruptTest(); //Thread starten th.start(); //2 Sekunden warten try { Thread.sleep(2000); } catch (InterruptedException e) { } //Thread unterbrechen th.interrupt(); private void printLine(int cnt) { } //Zeile ausgeben } System.out.print(cnt + ": "); 1: . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . for (int i = 0; i < 30; ++i) { 2: . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . System.out.print(i == cnt % 30 ? "* " : ". "); 3: . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . 4: . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . } 5: . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . System.out.println(); 6: . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . //100 ms. Warten – ganz schön lange 7: . . . . . . . * . . . . . . . . . . . . . . . . . . . . . . . . . . . . das . * .in. einem . . . .separaten . . . . . .Thread . . . . . . . . . try { • 8: .Programm, 9: . . . . . . . . . * . . . . . . . . . . . . . . . . . . . . Thread.sleep(100); ununterbrochen Textzeilen auf dem Bildschirm ausgibt. 10: . . . . . . . . . . * . . . . . . . . . . . . . . . . . . . } catch (InterruptedException e) { . . .Hauptprogramm . . . . . . . . * soll . . . . .Thread . . . . erzeugen . . . . . .und . . nach . • 11: Das den 12: . . . . . . . . . . . . * . . . . . . . . . . . . . . . . . interrupt(); 13: 2 . Sekunden . . . . . . durch . . . .einen . . * Aufruf . . . .von . . interrupt . . . . . eine . . . . . } 14: Unterbrechungsanforderung . . . . . . . . . . . . . . * . erzeugen. . . . . . . . . . . . . . . } 15: . . . . . . . . . . . . . . . * . . . . . . . . . . . . . . • 16: Der Thread soll dann die aktuelle Zeile fertig ausgeben . . . . . . . . . . . . . . . . * . . . . . . . . . . . . . terminieren. 17: und . . .anschließend . . . . . . . . . . . . . . * . . . . . . . . . . . . 18: . . . . . . . . . . . . . . . . . . * . . . . . . . . . . . 19: . . . . . . . . . . . . . . . . . . . * . . . . . . . . . . Weitere Methoden von Threads 1/2 19 o o o o sleep: staUsche Methode der Klasse Thread à der akUve Prozess pausiert für die angegeben Zeitspanne. Während der Wartezeit kann der ruhende Thread von einem anderen Thread durch Aufruf von interrupt unterbrochen, d.h. geweckt werden. Dies wird dem Thread durch eine InterruptedException signalisiert, damit er die Unterbrechung von der normalen Beendigung der sleep-­‐Methode unterscheiden kann. isAlive gibt immer dann true zurück, wenn der Thread gestartet, aber noch nicht wieder beendet wurde. Die Methode join wartet auf das Ende des Threads, für den sie aufgerufen wurde. Sie ermöglicht es damit, einen Prozess zu starten und (ähnlich einem FunkUonsaufruf) mit der weiteren Ausführung so lange zu warten, bis der Prozess beendet ist. public static void sleep(long millis) public static void sleep(long millis, int nanos) public final boolean isAlive() public final void join() throws InterruptedException Weitere Methoden von Threads 2/2 20 o Thread (Runnable target) Erzeugt ein neues Thread-­‐Objekt mit einem Runnable. o Runnable target, String name) Erzeugt ein neues Thread-­‐Objekt mit einem Runnable und setzt den Namen. o o getName() Liefert den Namen des Threads. Der Name wird im Konstruktor angegeben oder mit setName() zugewiesen. Standardmäßig ist der Name »Thread-­‐x«, wobei x eine eindeuUge Nummer ist. setName (String name) Ändert den Namen des Threads. public Thread (Runnable target) public Thread (Runnable target, String name) public final String getName () public final void setName (String name) Zustände eines Thread – vereinfachte Darstellung 21 unbekannt schlafend new interrupt sleep notify, interrupt blockiert wait, join erzeugt start rechenwillig Scheduler rechnend yield run() zu Ende beendet später noch genauer Threads -­‐ Beispiel 22 package threads; class CharPrinter extends Thread { char signal; public CharPrinter (char ch) { signal = ch;} public void run () { for (int i = 0; i < 20; i++) { System.out.print(signal); int delay = (int)(Math.random()*1000); try { sleep (delay); } catch (InterruptedException e) { return;} } Ausgabe: } } +++.*.*..*..*.**.**.**.*.***.***..**.*..... public class ThreadTest { public static void main(String[] args) { CharPrinter thread1 = new CharPrinter ('.'); CharPrinter thread2 = new CharPrinter ('*'); thread1.start(); thread2.start(); System.out.print("+++"); } } Thread-­‐Objekte 23 o o o Threads sind gewöhnliche Objekte mit beliebigen Membervariablen und Methoden. Jeder Thread (d.h. jedes Thread-­‐Objekt) hat seine eigenen lokalen Variablen. Greifen mehrere Threads auf Felder anderer Objekte zu, dann greifen sie auf dieselben Daten zu. ¤ ¤ ¤ Ein Thread kann Daten in einem Objekt ablegen, die dort von einem anderen Thread abgeholt werden. KommunikaUonsmedium für Threads. Der Zugriff auf die gemeinsamen Daten muss geordnet (synchronisiert) werden. Threads – Zugriff auf gemeinsame Daten 1/2 24 Thread 1 deposit (500) void deposit (int b) { Thread 2 withdraw (500) void withdraw (int b) { balance = balance + b; } balance = balance - b; } balance Kritische Bereiche Threads – Zugriff auf gemeinsame Daten 2/2 25 Thread 1 Thread 2 ... lies balance ... addiere 500 Speichere das Ergebnis in balance ... Welcher Wert steht in balance? lies balance subtrahiere 500 Speichere das Ergebnis in balance .... OrganisaUon der Parallelität 26 o o o o Parallel laufende Threads kommunizieren (indirekt) über den gemeinsam genutzten Speicher (= gemeinsam genutzte Objekte) è KoordinaUon ist erforderlich. è Sprachkonstrukte für die KoordinaUon; aber: diese Sprachkonstrukte führen selbst wieder zu neuen Problemen führen. Probleme in Threadprogrammes sind nur sehr schwer zu reproduzieren und damit zu testen à treten häufig erst im ProdukUvsystem auf. Grundproblem: Menschen können nur schlecht in parallelen Abläufen denken. Threads: Probleme 27 o Safety Hazards -­‐ das Programm verhält sich in Anwesenheit mehrerer Threads nicht mehr korrekt. ¤ race condiUon o Liveness Hazards -­‐ Probleme bei denen ein Programm mit mehreren Threads in einen Zustand gerät, bei dem es keine Fortschrije mehr machen kann. ¤ ¤ ¤ o Deadlock Livelock StarvaUon (Aushungern) Performance Hazards -­‐ Ein Programm das zwar korrekt funkUoniert, die Performance ist jedoch trotz mehrerer Threads schlecht. SynchronisaUon von Threads 28 o o o SynchronisaUon mit Monitoren. SynchronisaUon mit Ereignissen (wait, noUfy). SynchronisaUon auf das Ende von anderen Threads (join). Monitor: Konzept/Eigenschaqen 30 o o o o o o o o o o Monitor (in der InformaUk): ist ein programmiersprachliches Konzept zur SynchronisaUon von Zugriffen zeitlich verschränkt oder parallel laufender Prozesse oder Threads auf gemeinsam genutzten Datenstrukturen oder Ressourcen. Inkonsistente Zustände der Datenstrukturen werden vermieden. Monitor: Objekt (Sperrvariable), das besUmmte Ajribute in sich einschließt, auf die nur über besUmmte Zugangsmethoden zugegriffen werden kann. Es kann sich immer nur eine einziger Thread im Monitor befinden. Zu jeder Zugangsmethode verwaltet der Monitor eine Warteschlange. Betreten des Monitors: durch Ausführung einer Zugangsmethode à Monitor ist besetzt, Sperre wird gesetzt. Verlassen des Monitors: Sperre wird freigegeben. Monitor ist besetzt und ein anderer Thread will in den Monitor eintreten: der Thread wird in die Warteschlange eingetragen und muss warten, bis der Konkurrent den Monitor verlassen hat. Man kann keine Annahmen über die Reihenfolge machen, nach der die Threads aus der Warteschlange akUviert werden. Mit Monitoren werden kriUsche Bereiche gekapselt. Gute Systemauslastung ⇒ kriUsche Bereiche klein halten. Monitore: Realisierung in Java 31 o o Monitore werden realisiert mit synchronized-­‐Anweisung. Durch synchronized wird geschützt ¤ ein Block innerhalb einer Methode (synchronized-­‐Anweisung) n ¤ Der Eintrij in einen Monitor wird durch das Setzen einer Sperre auf einer Objektvariablen erreicht. eine kompleje Methode (synchronized-­‐Modifier) n Als Sperre wird der this-­‐Pointer verwendet. synchronized – Anweisung: zum Schutz eines Blocks 32 class KontoThread extends Thread { ... Nur wenn 2 Threads dasselbe Konto-Objekt zugreifen wollen, greift die Sperre. Account konto; public void run () { ... synchronized (konto) { konto.withdraw(1000); } ... synchronized (konto) { konto.deposit(500); } ... } ... } synchronized – Anwendung: Methode 33 class Account { ... int balance; synchronized void deposit (int b) { balance += b; } synchronized void withdraw (int b) { balance -= b; } ... } Sperre von Instanzmethoden mittels des this-Objekts void deposit (int b) { synchronized (this) { balance += b; } } void withdraw (int b) { synchronized (this) { balance -= b; } } SynchronisaUonsprobleme 34 Es kann vorkommen, dass sich ein Thread in einem Monitor befindet, aber nicht weiterlaufen kann, bevor eine besUmmte Bedingung erfüllt ist, die ein anderer Thread in dem geschützten Bereich vornimmt. Beispiel: o ... synchronized void withdraw (int b) { if (balance > b) balance -= b; else // warte bis Kontostand ausreichend } ... • Da der aktive Thread den Monitor blockiert, kann kein anderer Thread den Monitor betreten und dafür sorgen, dass die Bedingung erfüllt ist ⇒ Systemverklemmung (Deadlock), da keiner der Threads mehr weiterlaufen kann. SynchronisaUon mit wait und noUfy 1/2 36 o AusgangssituaUon (Monitor ist besetzt) ¤ ¤ Zusätzlich zu der Sperre, die einem Objekt zugeordnet ist, besitzt ein Objekt auch noch eine Warteliste: alle Threads, die vom Scheduler unterbrochen wurden und darauf warten, dass der Monitor wieder freigegeben wird. Ein Thread, der den Monitor besitzt, kann diesen wieder frei geben (z.B. wenn er auf das Eintreten einer besUmmten Bedingung warten muss). o Freigabe des Monitors ¤ wait nimmt die gewährten Sperren des akUven Threads (temporär) zurück und stellt den Thread, der den Aufruf von wait verursachte, in die Warteliste des Objekts. Dadurch wird er unterbrochen und im Scheduler als blockiert (d.h. wartend) markiert. SynchronisaUon mit wait und noUfy 2/2 37 o Aufwecken ¤ ¤ eines wartenden Threads notify enrernt einen (beliebigen) Thread aus der Warteliste des Objekts, stellt die (temporär) aufgehobenen Sperren wieder her und führt ihn dem normalen Scheduling (Zustand rechenwillig) zu. Gibt es keinen wartenden Prozess, bleibt notify wirkungslos. notifyAll enrernt alle Threads aus der Warteliste des Objekts, stellt die (temporär) aufgehobenen Sperren wieder her, d.h. es werden alle an diesem Objekt wartenden Threads informiert und können weitermachen. o Zulässig nur für gesperrte Objekte (innerhalb synchronized-­‐ Block). Zustände eines Thread -­‐ Details 38 blockiert Ende von synchronized new erzeugt start rechenwillig synchronized zuordnen verdrängen beendet yield notify, notifyAll, interrupt notify, notifyAll, interrupt, Fristablauf rechnend run() zu Ende join, wait wartend sleep, join mit Frist, wait mit Frist befristet wartend Abläufe steuern mit wait und noUfy 39 o o o o o wait und notify sind für elementare SynchronisaJonsaufgaben geeignet. Es kommt weniger auf die KommunikaUon als auf die Steuerung der zeitlichen Abläufe an. wait à immer in einer Schleife aufrufen, in der man die Bedingung prüq, auf die der Thread wartet. Beim Aufwachen des Threads kann nämlich nicht garanUert werden, dass diese Bedingung erfüllt ist. Der Thread muss deshalb die Bedingung nochmals prüfen und sich ggf. erneut schlafen legen. wait und notify dürfen nur aus einem synchronisierten Bereich (Monitor) aufgerufen werden (wenn das Objekt bereits gesperrt ist). Werden sie außerhalb eines synchronisierten Bereichs aufgerufen à Ausnahme (IllegalMonitorStateException). Während der Wartezeit (von wait) kann eine Unterbrechung durch einen anderen Thread auqreten (InterruptedExcepUon): à einschließen in eine try-­‐Anweisung. Wann muss synchronisiert werden? 40 o o o Mehrere Threads greifen auf ein Objekt zu + mindestens ein Thread ändert den Zustand des Objekts è alle Methoden die den Zustand des Objekts lesen oder schreibend zugreifen, müssen mit synchronized gekennzeichnet werden. Macht man dies nicht (d.h. mindestens eine Methode ist nicht mit synchronized gekennzeichnet) è dieses Objekt ist nicht Thread-­‐sicher. Die Zugriffe über die nicht synchronzied-­‐Methoden unterliegen keinerlei Kontrolle durch den Monitor und können jederzeit zum Zuge kommen à Safety Hazards. SynchronisaUon mit noUfyAll 41 noUfyAll staj noriy, wenn einer der folgenden Fälle gilt o In der Warteschlange befinden sich Threads mit unterschiedlichen Wartebedingungen. Verwendet man in diesem Fall noUfy à die Gefahr, dass der „falsche“ Thread geweckt wird. ¤ o Beispiel: Erzeuger-­‐Verbraucher-­‐Problem. Hier gibt es Threads, die darauf warten, dass der Puffer gefüllt wird und andere Threads, die darauf warten, dass im Puffer Platz ist. Durch die Veränderung des Zustands eines Objekts können mehrere Threads weiterlaufen. ¤ Beispiel: Ampel, an der mehrere Autos warten. Schaltet die Ampel auf grün, können alle wartenden Autos weiter fahren. Einsatzbereich von wait und noUfy 42 o Szenarien mit wait() und noUfy() sind oq Produzenten-­‐ Konsumenten-­‐Beispiele. ¤ ¤ ¤ ¤ Ein Thread liefert Daten, die ein anderer Thread verwenden möchte. Da er nicht in einer kostspieligen Schleife auf die InformaUon warten soll, synchronisieren sich die Partner über ein beiden bekanntes Objekt. Erst dann, wenn der Produzent sein OK gegeben hat, macht es für den Datennutzer Sinn weiterzuarbeiten; jetzt hat er seine benöUgten Daten. So wird keine unnöUge Zeit in Warteschleifen vergeudet, und der Prozessor kann die übrige Zeit anderen Threads zuteilen. SynchronisaUon auf Threadende mit join 43 o o o o join: Der aktuelle Thread wartet auf das Ende des Threads, für den join aufgerufen wurde. ⇒ man kann z.B. einen Thread starten und (ähnlich einem FunkUonsaufruf) mit der weiteren Ausführung so lange zu warten, bis der angegebene Thread beendet ist. join mit (long) Parameter: Der aktuelle Thread maximal die angegebene Zeit und fährt nach Ablauf der Zeit auch dann fort, wenn der andere Thread noch nicht beendet ist. Wenn ein Thread, für den join aufgerufen wurde schon beendet ist, dann kehrt join() sofort zurück. Anwendung: ¤ o große Probleme können in mehrere Teile zerlegt werden, wobei jedes Teilproblem von einem Thread gelöst wird (insbesondere bei Mehrprozessorsystemen sinnvoll). Zum Schluss müssen wir nur noch darauf warten, dass die Threads zum Ende gehen. Danach werden die Teilergebnisse eingesammelt: → join. Andere Lösung für zusammenlaufende Threads: zusammenzufassen dieser Threads in einer Thread-­‐Gruppe. Dann können sie zusammen behandelt werden, so dass nur das Ende der Thread-­‐Gruppe beobachtet wird. SynchronisaUon mit join –Beispiel 1/3 44 // Beispiel Bruce Eckel class Sleeper extends Thread { private int duration; public Sleeper(String name, int sleepTime) { super(name); duration = sleepTime; start(); } public void run() { try { sleep(duration); } catch (InterruptedException e) { System.out.println(getName() + " was interrupted. " + "isInterrupted(): " + isInterrupted()); return; } System.out.println(getName() + " has awakened"); } } SynchronisaUon mit join –Beispiel 2/3 45 class Joiner extends Thread { private Sleeper sleeper; public Joiner(String name, Sleeper sleeper) { super(name); this.sleeper = sleeper; start(); } public void run() { try { sleeper.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(getName() + " join completed"); } } SynchronisaUon mit join –Beispiel 3/3 46 public class Joining { public static void main(String[] args) { Sleeper sleepy = new Sleeper("Sleepy", 1500), grumpy = new Sleeper("Grumpy", 1500); Joiner dopey = new Joiner("Dopey", sleepy), doc = new Joiner("Doc", grumpy); grumpy.interrupt(); } } Grumpy was interrupted. isInterrupted(): false Doc join completed Sleepy has awakened Dopey join completed Threads: Probleme 47 o Safety Hazards -­‐ das Programm verhält sich in Anwesenheit mehrerer Threads nicht mehr korrekt. ¤ race condiUon o Liveness Hazards -­‐ Probleme bei denen ein Programm mit mehreren Threads in einen Zustand gerät, bei dem es keine Fortschrije mehr machen kann. ¤ Deadlock Livelock ¤ StarvaUon (Aushungern) ¤ o Es passiert nichts mehr. Permanenter Wechsel zwischen 2 Zuständen, ohne dass etwas passiert Es werden immer andere Threads bevorzugt. Performance Hazards -­‐ Ein Programm das zwar korrekt funkUoniert, die Performance ist jedoch trotz mehrerer Threads schlecht. Thread Sicherheit 48 o o o o o Eine Klasse ist thread safe, wenn sie auch bei der Verwendung mehrerer Threads keine der genannten Probleme zeigt. Viele Klassen der Klassenbibliothek sind nicht thread safe (z.B. SimpleDateFormat) und müssen explizit geschützt werden. Unveränderliche (immutable) Objekte sind immer thread safe (z.B. String). Die meisten CollecUons sind nicht thread safe, können aber mit Collec8ons.synchronizedCollec8on() entsprechend verpackt werden. Von einigen Klassen gibt es zwei Varianten (StringBuffer, StringBuilder) -­‐ eine threadsafe, die andere nicht. è Java Memory Model Java Memory Model 1/3 49 ... regelt 3 Dinge o Atomicity: Welche OperaUonen sind atomar, d.h. werden nicht durch andere Threads unterbrochen? o Ordering: In welcher Reihenfolge werden die AkUonen abgewickelt? o Visibility: Wann werden ModifikaUonen im Speicher anderen Threads sichtbar gemacht? Java Memory Model 2/3 50 o o Es gibt keine sog. SequenJal Consistency: Ein Thread, der später drankommt, kann sehen, was die Threads vor ihm im Speicher an den gemeinsam verwendeten Daten gemacht haben. Das ist ein wunderbar simples mentales Modell, das aber leider von Java nicht unterstützt wird. Es gibt in der SprachspezifikaUon stajdessen eine Reihe von GaranUen für die Reihenfolge von OperaUonen und auch für die Sichtbarkeit von Memory-­‐ModifikaUonen, aber sie sind wesentlich schwächer als unserer IntuiUon entsprechende SequenUal Consistency. Java Memory Model 3/3 51 o Definiert, wie sich die Java VM bezüglich des Speichers verhalten muss. ⇒ Pla{ormunabhängigkeit der Sprache. ⇒ Unterschiedliches Verhalten der Pla{ormen (z.B. IBM vs. Intel). o o o Forderung„within-­‐thread as-­‐if-­‐serial“: der Speicher muss sich (in einem Threads) so verhalten als ob die InstrukUonen sequenUell ausgeführt würden. Die VM darf beliebige OpUmierungen machen, solange diese GaranUe erhalten bleibt. Unteilbar sind nur sog. atomare OperaUonen. Atomare OperaUonen 52 o o Eine atomare OperaUon kann nicht von einem anderen Thread gestört werden. Sie findet immer ganz oder gar nicht staj. Ohne explizite SynchronisaUon sind nur folgende OperaUonen in Java atomar: ¤ ¤ o Nicht atomar sind ¤ ¤ o Lesen eines 32-­‐Bit Feldes (byte, short, int, float, char, boolean). Schreiben eines 32-­‐Bit Felds (byte, short, int, float, char, boolean). Lesen oder Schreiben eines 64bit Feldes (long, double) InkremenUeren oder DekremenUeren eines 32bit Feldes; int i = 0; i++; Es gibt spezielle Klassen, um komplexere atomare OperaUonen zu programmieren (java.u8l.concurrent.atomic.*). Java Memory Model (Folgerung): Beispiel 53 o o o Es ist nicht gesichert, dass die OperaUonen auf 64-­‐Bit-­‐Datentypen unteilbar sind; eine 64-­‐Bit-­‐Zuweisung lässt sich aus zwei 32-­‐Bit-­‐ Zuweisungen zusammensetzen. Es kann also passieren, dass ein Thread mijen in einer long-­‐ oder double-­‐ OperaUon von einem anderen Thread verdrängt wird. Greifen zwei Threads auf die gleiche 64-­‐Bit-­‐Variable zu, so könnte möglicherweise der eine Thread eine Hälqe schreiben und der andere Thread die andere. a = 0x00000000 00000000 Thread-­‐1: a = 0 Thread-­‐2: a = -­‐1 Mögliche Ergebnisse: o o o o a a a a = = = = 0x00000000 0xFFFFFFFF 0x00000000 0xFFFFFFFF a = 0xFFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF 00000000 VolaUle Variablen 54 o o o o Die GaranUen des Java Memory Models gelten nur für einen Thread, nicht aber über Threads hinweg. Der Modifier vola8le erweitert die GaranUen für eine Variable bei mehreren Threads, die auf diese Variable zugreifen: atomarer Lese-­‐/Schreibzugriff. Ein Thread der von einer vola8le Variablen liest, sieht immer den letzten und aktuellen Wert, den ein anderer Thread hineingeschrieben hat. Für die Java VM bedeutet das ¤ ¤ ¤ volaUle Variablen dürfen nicht in Registern abgelegt werden. volaUle Variablen dürfen nicht anderweiUg gecached werden. volaUle Variablen erzwingen einen Speicherschutz. within-­‐thread as-­‐if-­‐serial: Beispiel 55 long i, k, m; i = 15 i++; k = 18; k++; m = i + k; Ergebnis nur innerhalb eines Threads identisch. long i, k, m; i = 15 k = 18; k++; i++; m = i + k; volaUle: Beispiel 56 int cnt; … int i = cnt; Thread.sleep( 10000 ); int j = cnt; o o Ergebnis auch Threadübergreifend identisch. volatile int cnt; … int i = cnt; Thread.sleep( 10000 ); int j = cnt; Verändert ein anderer Thread während des Wartens die Variable cnt, könnte dennoch i = j sein, wenn die Laufzeitumgebung die Variable j nicht frisch mit dem Wert aus cnt iniUalisiert. Variable cnt mit volaUle gekennzeichnen è das Ergebnis wird nicht im Zwischenspeicher (Thread-­‐Cache) belassen, sondern ständig aktualisiert. ¤ Die parallelen Threads sehen somit immer den korrekten Variablenwert, da er vor jeder Benutzung aus dem Speicher gelesen und nach einer Änderung sofort wieder zurückgeschrieben wird. Interface Runnable 1/2 57 o Es ist nicht immer möglich, eine Klasse, deren Exemplare als Thread laufen sollen, von Thread abzuleiten. ¤ ¤ ¤ o o Dies ist insbesondere dann nicht möglich, wenn die Klasse Bestandteil einer Vererbungshierarchie ist, die eigentlich nichts mit MulUthreading zu tun hat. Da Java keine Mehrfachvererbung kennt, kann eine bereits abgeleitete Klasse nicht von einer weiteren Klasse erben. Da sehr unterschiedliche Klassen als Thread parallel zu vorhandenem Code ausgeführt werden können, ist dies eine sehr unschöne Einschränkung des MulUthreading-­‐Konzepts von Java. Ausweg: Threads nicht durch Ableiten aus Thread, sondern durch ImplemenUerung des Interfaces Runnable zu erzeugen. Jede Klasse, deren Exemplare als Thread laufen sollen, muss das Interface Runnable implemenUeren (sogar die Klasse Thread selbst). ¤ Runnable enthält nur eine einzige DeklaraUon: die der Methode run. Interface Runnable 2/2 58 o Um eine nicht von Thread abgeleitete Instanz in dieser Weise als Thread laufen zu lassen, ist in folgenden Schrijen vorzugehen: ¤ ¤ ¤ è Erzeugen eines neuen Thread-­‐Objekts. An den Konstruktor wird das Objekt übergeben, das parallel ausgeführt werden soll. Die Methode start des neuen Thread-­‐Objekts wird aufgerufen. Das Thread-­‐Objekt startet die run-­‐Methode des übergebenen Objekts, das sie durch die Übergabe im Konstruktor kennt. Da dieses Objekt das Interface Runnable implemenUert, ist garanUert, dass eine geeignete Methode run zur Verfügung steht. public Thread(Runnable target) Erzeugt einen neuen Thread für die Runnable-Implementierung target. Interface Runnable -­‐ Beispiel 59 // Beispiel: Bruce Eckel - SimpleThread using the Runnable interface. public class RunnableThread implements Runnable { private int countDown = 5; public String toString() { return "#" + Thread.currentThread().getName() + ": " + countDown; } public void run() { while(true) { System.out.println(this); if(--countDown == 0) return; } } public static void main(String[] args) { for(int i = 1; i <= 5; i++) new Thread(new RunnableThread(), "" + i).start(); } } 60 Threads erweitern oder Interface Runnable implemenUeren? Vorteile Nachteile Threads erweitern Programmcode in run() kann die Methoden der Klasse Thread nutzen. Da es in Java keine Mehrfachvererbung gibt, kann die Klasse nur Thread erweitern. Unnatürliche Klassenhierarchie. Interface Runnable implementieren Die Klasse kann von einer anderen, problemspezifischen Klasse erben. Kann sich nur mit Umwegen selbst starten; allgemein: ThreadMethoden können nur über Umwege genutzt werden. Hintergrundprozesse – Dämonen (Daemons) 61 o o o o o o o Für die bisherigen Programmen gilt: ein gestarteter Thread, sofern er eine Endlosschleife (wie z.B. ein Server) enthält, wird nie beendet. Wenn run() nie terminiert, so läuq der Thread immer weiter, auch wenn die HauptapplikaUon beendet ist. Dies ist nicht immer beabsichUgt, wenn die ApplikaUon beendet ist. Dann sollte auch der endlose Thread beendet werden. Ein im Hintergrund arbeitender Thread erhält eine spezielle Kennung: er wird als Dämon gekennzeichnet. Standardmäßig ist ein Thread kein Dämon. Wenn das Hauptprogramm beendet ist und die Laufzeitumgebung erkennt, dass kein normaler Thread läuq, sondern nur Dämonen, dann werden diese (gewissermaßen von außen) terminiert. setDaemon() Daemon Thread setDaemon(false) Daemon -­‐ Beispiel 62 // Beispiel von Bruce Eckel // Daemon threads don't prevent the program from ending. public class SimpleDaemons extends Thread { public SimpleDaemons() { setDaemon(true); // Must be called before start() start(); } public void run() { while(true) { try { sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(this); } } public static void main(String[] args) { for(int i = 0; i < 10; i++) new SimpleDaemons(); } } Daemon -­‐ Beispiel 63 class DaemonThread extends Thread { DaemonThread () { setDaemon( true ); } public void run() { while ( true ) ; } public static void main( String args[] ) { new DaemonThread().start(); } } Verwalten von Threads 1/2 64 o o Threads besitzen auch eine Reihe administraUver Eigenschaqen. Eigenschaqen, die bei den Threads selbst zu finden sind, beispielsweise die Priorität oder der Name eines Threads: ¤ ¤ Jeder Thread in Java hat einen eindeuUgen Namen. Wird dieser nicht explizit vergeben (beispielsweise im Konstruktor), so vergibt Java einen automaUsch generierten Namen der Form "Thread-­‐" + n, wobei n ein int ist. AlternaUv kann einem Thread mit der Methode setName auch vom Programm selbst ein Name zugewiesen werden. Neben einem Namen besitzt ein Thread auch eine Priorität. Die Priorität steuert den Scheduler in der Weise, dass bei Vorhandensein mehrerer bereiter Threads diejenigen mit höherer Priorität vor denen mit niedrigerer Priorität ausgeführt werden. Beim Anlegen eines neuen Threads bekommt dieser die Priorität des aktuellen Threads. Mit Hilfe der Methode setPriority kann die Priorität gesetzt und mit getPriority abgefragt werden: java.lang.Thread Verwalten von Threads 2/2 65 o Jeder Thread in Java gehört zu einer Thread-­‐Gruppe, die untereinander über Vater-­‐Kind-­‐Beziehungen miteinander in Verbindung stehen: ¤ ¤ Thread-­‐Gruppen dienen dazu, InformaUonen zu verwalten, die nicht nur für einen einzelnen Thread von Bedeutung sind, sondern für eine ganze Gruppe. Weiterhin bieten sie Methoden, die auf alle Threads einer Gruppe angewendet werden können, und sie erlauben es, die Threads einer Gruppe aufzuzählen. In der Java-­‐Klassenbibliothek gibt es zu diesem Zweck eine eigene Klasse ThreadGroup. Der EinsUeg zur Anwendung von Objekten der Klasse ThreadGroup ist die Methode getThreadGroup der Klasse Thread. Sie liefert das ThreadGroup-­‐Objekt, dem der aktuelle Thread angehört. Alle Thread-­‐ Gruppen sind über eine Vater-­‐Kind-­‐Beziehung miteinander verkejet. Mit Hilfe der Methode getParent von ThreadGroup kann die Vater-­‐ Thread-­‐Gruppe ermijelt werden. java.lang.ThreadGroup 66 AutomaUsches Starten eines Thread bei der Erzeugung class ThreadAutoStart implements Runnable { ThreadAutoStart() { new Thread( this ).start(); } public void run() { . . . } } Ein Objekt verwaltet seinen eigenen Thread, der im Konstruktor gestartet wird. Dort muss ein Thread-Objekt für die Runnable-Umgebung angelegt und die start()-Methode ausgeführt werden. Klassen und Interfaces der Concurrency UUliUes 67 o ⇒ ⇒ o Bisher (und vor Java 5): Interface Runnable, Klasse Thread + ein paar SynchronisaUonsmechanismen. Vorteil: Elegantes, flexibel nutzbares und schlankes Konzept. Nachteil: Man muss für wiederkehrende Aufgaben eigene Klassen schreiben (Queue-­‐Klassen für Erzeuger-­‐Verbraucher-­‐ Anwendungen), Klassen zum Poolen von Threads, Semaphore, etc. Seit Java 5: Eine Reihe von Bibilotheksklassen für Probleme, die man bisher selbst implemenUeren musste. à java.util.concurrent Grundidee der Concurrency UUliUes 68 o o Programmierern werden ferUge Bausteine für die Anwendungsent-­‐ wicklung zur Verfügung gestellt (ähnlich wie das CollecUon Framework). Bestandteile der Concurrency UUliUes: ¤ Concurrent CollecUons n ¤ Executor Framework n ¤ Flexiblere ObjektrepräsentaUon des synchronized Schlüsselworts sowie der java.lang.Object Monitor Methoden wait(), noUfy() und noUfyAll(). Atomic Variables n o Diverse Hilfsklassen zur KoordinaUon mehrerer Threads, zum Beispiel Semaphore oder CyclicBarrier. Locks und CondiUons n ¤ Ein Framework zur Ausführung asynchroner Tasks durch Threadpools. Synchronizers n ¤ Spezielle ImplemenUerungen der CollecUon Framework Interfaces Map, Set, List, Queue, Deque. Erlauben die atomare ManipulaUon einzelner Variablen (CAS -­‐ Compare And Set) zur ImplemenUerung von Algorithmen ohne Sperren (lock-­‐free algorithms). Für die gängigen Fälle in der Anwendungsentwicklung besitzen die Concurrent CollecUons sowie das Executor Framework mit Sicherheit einen hohen Stellenwert. FerUge Lösungen des concurrent-­‐Paketes 69 o Sperrkonzept, das nicht mehr an die Blockstruktur gebunden ist. ¤ o Feingranulare ThreadsynchronisaUon und Steuerung ¤ o ¤ o Executor Future Producer/Consumer mit BlockingQueue Performante, threadsichere CollecUons ¤ ¤ o Barrier, Latch, Semaphore Parallele Tasks und deren Ausführungssteuerung ¤ o Lock, ReentrantLock ConcurrentHashMap CopyOnWriteArrayList Eigene komplexere atomare OperaUonen mit java.uUl.concurrent.atomic erstellen. Sperren (Interface Lock) -­‐ Ausschnij 70 o void lock() ¤ o Versucht in der angegebenen Zeitspanne den Lock zu bekommen. Das Warten kann mit interrupt() auf dem Thread unterbrochen werden, was tryLock() mit einer ExcepUon beendet. void unlock() ¤ o Wenn der kriUsche Abschnij sofort betreten werden kann, ist die FunkUonalität wie bei lock() und die Rückgabe ist true. Ist der Lock gesetzt, so wartet die Methode nicht wie lock(), sondern kehrt mit einem false zurück. boolean tryLock( long Ume, TimeUnit unit ) throws InterruptedExcepUon ¤ o Wartet so lange, bis der kriUsche Abschnij betreten werden kann, und markiert ihn dann als betreten. boolean tryLock() ¤ o java.util.concurrent.locks.Lock Verlässt den kriUschen Block. void lockInterrupUbly() throws InterruptedExcepUon ¤ Wartet wie lock(), um den kriUschen Abschnij betreten zu dürfen, kann aber mit einem interrupt() von außen abgebrochen werden. (Der lock()-­‐Methode ist ein Interrupt egal.) ImplemenUerende Klassen müssen diese Vorgabe nicht zwingend umsetzen, sondern können die Methode auch mit einem einfachen lock() realisieren. ReentrantLock implemenUert lockInterrupUbly() erwartungsgemäß. ReentrantLock (interface) -­‐ Ausschnij 71 o ReentrantLock() ¤ o Anfrage, ob der Lock gerade genutzt wird und im Moment kein Betreten möglich ist. final int getQueueLength() ¤ o Erzeugt ein neues Lock-­‐Objekt mit fairem Zugriff, gibt also dem am längsten Wartenden den ersten Zugriff. boolean isLocked() ¤ o Erzeugt ein neues Lock-­‐Objekt, das nicht unbedingt dem am längsten Wartenden den ersten Zugriff gibt. ReentrantLock( boolean fair ) ¤ o java.util.concurrent.locks.ReentrantLock Ermijelt, wie viele auf das Betreten des Blocks warten. int getHoldCount() ¤ Gibt die Anzahl der erfolgreichen lock()-­‐Aufrufe ohne passendes unlock() zurück. Sollte nach Beenden des Vorgangs 0 sein. Ausführung von Tasks -­‐ Executors 72 {Interface} Bietet die Funktionalität, eine Tätigkeit, die Runnable implementiert hat, auszuführen. ExecutorService • Service Management Funktionalität, um den Service zu beenden, seinen Status abzufragen, etc., Executor {Interface} • die Möglichkeit Callables ausführen zu lassen. Abstract ExecutorService • Threadpool = Abstraktion, die eine Anzahl von erzeugten und bereits gestarteten Threads vorrätig hält, um mit ihnen asynchrone Tätigkeiten auszuführen. Bsp.: Ein Application Server wird einen Threadpool enthalten, um die hereinkommenden Serviceanfragen asynchron damit auszuführen. {abstract} ThreadPoolExecutor Klassen und Interfaces im Zusammenhang mit dem Threadpool Ausführung von Tasks -­‐ Executors 73 o Threads ¤ ¤ ¤ o Zur Ausführung eines Runnable à es muss immer ein neues Thread-­‐ Objekt aufgebaut werden, denn start() ist nur einmal auf einem Thread-­‐Objekt erlaubt (sonst: java.lang.IllegalThreadStateException). Würde derselbe Thread während seines Lebenszyklus eine Reihe unterschiedlicher Runnable abarbeiten wollen à andere Umsetzung ist nöUg. Wenn das Runnable nicht sofort, sondern später (Ostern) oder wiederholt (immer Weihnachten) ausgeführt werden soll à andere Umsetzung ist nöUg. Excecutor ¤ Führt Tasks in eigenem Threads aus. Übernimmt das Pooling der Threads. ¤ Erlaubt Tasks für spätere Zeiträume einzuplanen. ¤ Executors (Ausführer) 74 o o o o o Im Package: java.util.concurrent als Schnijstelle vereinbart. AbstrakUon für Klassen, die Befehle über Runnable ausführen. void execute(Runnable command) -­‐ wird später von Klassen implemenUert, die ein Runnable abarbeiten können. Jeder, der nun Befehle über Runnable abarbeitet, ist Executor. Von dieser Schnijstelle gibt es bisher zwei wichUge ImplemenUerungen: ¤ ¤ o ThreadPoolExecutor. Die Klasse baut eine Sammlung von Threads auf, den Thread-­‐Pool. Ausführungsanfragen werden von den freien Threads übernommen. ScheduledThreadPoolExecutor. Eine Erweiterung von ThreadPoolExecutor um die Fähigkeit, zu besUmmen Zeiten oder mit besUmmten Wiederholungen Befehle abzuarbeiten. Die beiden Klassen haben nicht ganz so triviale Konstruktoren; eine UUlity-­‐Klasse (Executors)vereinfacht den Au}au dieser speziellen Executor-­‐Objekte. Executor (interface) 75 o Führt eine übergebene Runnable Task aus. Entkopplung der Task-­‐Ausführung von den Mechanismen des Task-­‐ Managements (inkl. Details der Thread-­‐ Verwendung oder Scheduling). Ein Executor ersetzt die explizite Erzeugung von Threads. o Beispiel: o o new Thread(new(RunnableTask())).start() wird ersetzt durch Executor executor = anExecutor; executor.execute(new RunnableTask1()); executor.execute(new RunnableTask2()); etc.