Threads in Java Wiederholung der BS Grundlagen Alois Schütte AOSD 1 Threads: Erzeugen von Threads Beim Start eines Java Programms wird ein Prozess erzeugt, der einen Thread enthält, der die Methode main der angegebenen Klasse ausführt. Der Code weiterer Threads muss in einer Methode mit Namen run realisiert werden. public void run() { // Code wird in eigenem Thread ausgeführt } Ein Programm, das Threads erzeugt, erbt von der Klasse Thread und überschreibt die Methode run(): $ cat MyThread.java public class MyThread extends Thread { public void run() { System.out.println("Hello World"); } } public static void main(String[] args) { MyThread t = new MyThread(); t.start(); } } $ Alois Schütte AOSD 2 Threads: Erzeugen von Threads Wiederholung Interfaces Bei Interfaces handelt es sich um eine Abart der abstrakten Klassendeklaration. Es enthält neben Datenelementen abstrakte Methoden. Sie werden u.a. für die Mehrfachvererbung eingesetzt, denn Klassen können mehrere Schnittstellen implementieren. Implementiert eine Klasse ein Interface, so muss sie alle Methoden des Interface überschreiben. Beispiel: 2 Interfaces, eine Klasse implementiert beide Interfaces und ist von einer Basisklasse abgeleitet: $ cat MyInterface.java interface MyInterface1 { String s1 = "MyInterface1"; public void print1(); } interface MyInterface2 { String s2 = "MyInterface2"; public void print2(); } class MySuperClass { protected String str = "MySuperClass "; } Alois Schütte AOSD 3 Threads: Erzeugen von Threads -> Wiederholung Interfaces class MySubClass extends MySuperClass implements MyInterface1, MyInterface2 { public void print1() { System.out.println(str + s1); } public void print2() { System.out.println(str + s2); } } public class MyInterfaces { public static void main(String[] args) { MySubClass object = new MySubClass(); object.print1(); object.print2(); } } $ MySubClass ist von MySuperClass abgeleitet und implementiert die beiden Schnittstellen, somit ist der Zugriff auf alle print-Methoden möglich. Alois Schütte AOSD 4 Threads: Erzeugen von Threads Threads in komplexen Klassenhierarchien Wenn sich die Methode run() in einer Klasse befinden soll, die selbst bereits aus einer anderen Klasse abgeleitet ist, so kann diese Klasse nicht zusätzlich von Thread abgeleitet werden (Java unterstützt keine Implementierungs-Mehrfachvererbung). In diesem Fall kann das Interface Runnable des Package java.lang verwendet werden: $ cat MyRunnableThread.java public class MyRunnableThread implements Runnable { public void run() { System.out.println("Hello World"); } public static void main(String[] args) { MyRunnableThread runner = new MyRunnableThread(); Thread t = new Thread(runner); t.start(); } } $ Alois Schütte AOSD 5 Threads: Threadtermination Ein Thread terminiert, wenn seine run()-Methode (bzw. die Methode main() im Fall des Ursprungs-Thread) beendet ist. Sind alle von einem Prozess initiierten Threads beendet, so terminiert der Prozess (falls es kein Dämon ist). Die Klasse Thread stellt eine Methode isAlive bereit, mit der abfragbar ist, ob ein Thread noch lebt (schon gestartet und noch nicht terminiert ist). Damit könnte aktives Warten etwa wie folgt programmiert werden (man sollte es so aber nie tun, da aktives Warten sehr rechenintensiv ist): MyThread t = new myThread(); t.start(); while (t.isAlive()); // hier ist jetzt: t.isAlive == false, der Thread t ist terminiert Wenn in einer Anwendung auf das Ende eines Thread gewartet werden muss, etwa um die Rechenergebnisse des Thread weiterzuverarbeiten, kann die Methode join der Klasse Thread benutzt werden. Der Thread wird blockiert, bis der Thread, auf den man wartet, beendet ist. MyThread t = new myThread(); t.start(); t.join(); // blockiert, bis t beendet ist. // auch hier ist jetzt: t.isAlive == false, der Thread t ist terminiert Alois Schütte AOSD 6 Threads: Abarbeitungsreihenfolge Werden mehrere Threads erzeugt, so ist die Ausführungsreihenfolge nicht vorhersehbar! $ cat Loop1.java public class Loop1 extends Thread { private String myName; public Loop1(String name) { myName = name; } public void run() { for(int i = 1; i <= 10000; i++) System.out.println(myName + " (" + i + ")"); } public static void main(String[] args) { Loop1 t1 = new Loop1("Thread 1"); Loop1 t2 = new Loop1("Thread 2"); Loop1 t3 = new Loop1("Thread 3"); t1.start(); t2.start(); t3.start(); } } $ Alois Schütte AOSD $ java Loop1 … Thread 1 (7823) Thread 2 (8886) Thread 1 (7824) Thread 2 (8887) Thread 1 (7825) Thread 2 (8888) Thread 1 (7826) Thread 3 (6647) Thread 2 (8889) Thread 3 (6648) Thread 2 (8890) Thread 3 (6649) Thread 2 (8891) 7 Threads: Synchronisation Problem des gemeinsamen Zugriffs Wenn mehrere Threads gemeinsam auf Daten zugreifen, müssen sich die einzelnen Threads darüber „verständigen“, wer wann was machen darf. Sie müssen ihre Aktivitäten synchronisieren. Beispiel: Klasse Even stellt sicher, dass n nur gerade sein kann: $ cat Even1.java class Even { // POST: n is always even private int n = 0; public int next() { ++n; try { Thread.sleep(100); } catch (InterruptedException e) {}; ++n; return n; } } Alois Schütte AOSD 8 Threads: Synchronisation - > Problem des gemeinsamen Zugriffs Das Programm mit einem Thread funktioniert problemlos, also ist die Klasse offensichtlich korrekt implementiert, oder? public class Even1 extends Thread { private Even e; public Even1(Even e) { this.e = e; } public void run() { for (int i = 1; i <= 10; i++) { System.out.println("result: " + e.next()); } } public static void main(String[] args) { Even e = new Even(); Even1 t1 = new Even1(e); t1.start(); } } $ Alois Schütte AOSD $ java Even1 result: 2 result: 4 result: 6 result: 8 result: 10 result: 12 result: 14 result: 16 result: 18 result: 20 $ 9 Threads: Synchronisation - > Problem des gemeinsamen Zugriffs Wenn mehrere Threads sich ein Even-Objekt teilen, kommt es zu unerwarteten Ausgaben (ungeraden Werten). Wie ist das erklärbar? public class Even2 extends Thread { private Even e; public Even2(Even e) { $ java Even2 this.e = e; result: 3 } result: 4 public void run() { result: 7 for (int i = 1; i <= 10; i++) { result: 8 System.out.println("result: " + e.next()); result: 11 result: 12 } result: 15 } public static void main(String[] args) { Even e = new Even(); Even2 t1 = new Even2(e); Even2 t2 = new Even2(e); t1.start(); t2.start(); } } $ Alois Schütte AOSD result: 16 result: 19 result: 21 result: 23 result: 24 result: 27 result: 28 result: 31 result: 33 result: 35 result: 37 result: 39 result: 40 $ 10 Threads: Synchronisation Synchronized Methoden und Blöcke Die Java-Superklasse Object beinhaltet als Eigenschaft eine Sperre. Da jede Klasse von Object abgeleitet ist, besitzen alle Klassen diese Eigenschaft. Das Sperren gehorcht dem „acquire-release Protokoll“: Die Sperre wird gesetzt (acquire), beim Betreten einer synchronized-Methode (oder einer synchronized Blocks) und entfernt (release) beim Verlassen des Blocks (auch beim Verlassen durch eine exception). Sperre Thread A Thread B Objekt Wird eine Methode einer Klasse mit synchronized gekennzeichnet, so muss diese Sperre zuerst gesetzt werden, bevor die Methode ausgeführt wird, hier initiiert von Thread A. Hat ein anderer Thread A die Sperre bereits gesetzt (seine Methode ist in Ausführung), so wird der aufrufende Thread B blockiert. Das Blockieren ist aber nicht durch aktives Warten realisiert, sondern der Thread A wird beim Thread-Umschalten nicht mehr berücksichtigt. Wenn die Methode des Thread A beendet ist, wird die Sperre entfernt und der Thread B wird beim Scheduling wieder berücksichtigt. Alois Schütte AOSD 11 Threads: Synchronisation -> Synchronized Methoden und Blöcke Somit nun die korrekte Implementierung der Klasse Even: $ cat Even3.java class Even { // POST: n is always even private int n = 0; public synchronized int next() { ++n; try { Thread.sleep(100); } catch (InterruptedException e) {}; ++n; return n; } } … Alois Schütte AOSD $ java Even3 result: 2 result: 4 result: 6 result: 8 result: 10 result: 12 result: 14 result: 16 result: 18 result: 20 result: 22 result: 24 result: 26 result: 28 result: 30 result: 32 result: 34 result: 36 result: 38 result: 40 $ 12 Threads: Synchronisation -> Synchronized Methoden und Blöcke Neben der Markierung synchronized für Methoden, kann man auch einen einzelnen Block markieren: public void buchen(int kontonr, float betrag) { synchronized(konten[kontonr]) { float alterStand = konten[kontonr].abfragen(); float neuerStand = alterStand + betrag; konten[kontonr].setzen(neuerStand); } } Hier wird die Sperre auf das Objekt konten[kontonr] angewendet. Generell gilt folgende Regel zur Verwendung von synchronized: Wenn von mehreren Threads auf ein Objekt zugegriffen wird, wobei mindestens ein Thread den Zustand (repräsentiert durch die Werte der Attribute) des Objekts ändert, dann müssen alle Methoden, die auf den Zustand lesend oder schreibend zugreifen, mit synchronized gekennzeichnet werden. Alois Schütte AOSD 13 Threads: wait und notify In vielen Anwendungssituationen ist es erforderlich, dass eine Methode nur dann ausgeführt wird, wenn zusätzlich zum konsistenten Zustand weitere anwendungsspezifische Bedingungen erfüllt sind. Das Prüfen dieser Bedingungen durch aktives Warten (polling) belastet die CPU intensiv und nicht zu empfehlen. Lösung: Methoden wait und notify der Klasse Objekt Ein wait bewirkt die folgenden Aktionen: 1. wenn der laufende Thread unterbrochen wurde, wird die Ausnahme InterruptedException erzeugt. Andernfalls (Normalfall) wird der laufende Thread blockiert. 2. Die JVM fügt den laufenden Thread in eine Menge (wait set) ein, die mit dem Objekt assoziiert ist. 3. Der synchronization Lock für das Objekt wird freigegeben (released), alle anderen Locks bleiben erhalten. Ein notify bewirkt die folgenden Aktionen: 1. Ein zufälliger Thread t wird aus dem wait set des Objektes ausgewählt. 2. Thread t muss den Lock des Objektes wieder erhalten, d.h. er blockiert solange, bis der Thread der notify aufgerufen hat, den Lock besitzt oder bis ein anderer Thread, der den Lock hält, ihn freigegeben hat. 3. Thread t wird nach erhalten des Lock nach seinem wait weitermachen. Ein notifyAll arbeitet genauso, nur dass alle Threads im wait set ausgewählt werden (Achtung: nur einer kann aber weitermachen, da die anderen ja auf den Erhalt des Lock warten). Alois Schütte AOSD 14 Threads: wait und notify T1 class X { synchronized void w() { before();// some actons wait(); // Thread.wait after(); // some actions } begin x.w() acuire lock before(); wait(); release lock enter wait set synchronized void n() { notify();// Thread.notify } T2 T3 begin x.w() blocks acuire lock before(); wait(); release lock enter wait set void before {...} void after {...} begin x.n() wait for lock aquire lock notify() release lock } exit wait set wait lor lock exit wait set wait lor lock aquire lock after() release lock aquire lock after() release lock T1 T2 waiting set Alois Schütte AOSD 15 Threads: wait und notify Probleme mit wait() und notify() entstehen, wenn mehrere Threads in der Warteschlange stehen und der falsche Thread geweckt wird. Dies wird am ErzeugerVerbraucher Problem demonstriert. buffer producer put() class Buffer { private boolean available=false; private int data; public synchronized void put(int x) { while(available) { try { wait(); } catch(InterruptedException e) {} } data = x; available = true; notify(); } consumer get() public synchronized int get() { while(!available) { try { wait(); } catch(InterruptedException e) {} } available = false; notify(); return data; } } // end Buffer Alois Schütte AOSD 16 Threads: wait und notify class Producer extends Thread { private Buffer buffer; private int start; public Producer(Buffer b, int s) { buffer = b; start = s; } public void run() { for(int i = start; i < start + 100; i++) { buffer.put(i); } } } class Consumer extends Thread { private Buffer buffer; public Consumer(Buffer b) { buffer = b; } public void run() { for(int i = 0; i < 100; i++) { int x = buffer.get(); System.out.println("got " + x); } } } Alois Schütte AOSD 17 Threads: wait und notify public class ProduceConsume { public static void main(String[] args) { Buffer b = new Buffer(); Consumer c = new Consumer(b); Producer p = new Producer(b, 1); c.start(); p.start(); } } $ java ProduceConsume got 1 got 2 got 3 got 4 got 5 … got 100 $ Insgesamt ist die Ausgabe, so wie wir das erwartet haben. Alois Schütte AOSD 18 Threads: wait und notify Nun werden mehrere Erzeuger und Verbraucher gestartet. $ cat ProducerConsumer2.java public class ProduceConsume2 { public static void main(String[] args) Buffer b = new Buffer(); Consumer c1 = new Consumer(b); Consumer c2 = new Consumer(b); Consumer c3 = new Consumer(b); Producer p1 = new Producer(b, 1); Producer p2 = new Producer(b, 101); Producer p3 = new Producer(b, 201); c1.start(); c2.start(); c3.start(); p1.start(); p2.start(); p3.start(); } } { $ java ProduceConsume2 got 1 got 102 got 2 got 101 got 104 got 103 got 105 Das Programm bleibt stehen, es passiert nichts mehr; es wird keine neue Ausgabe mehr erzeugt, das Programm ist aber noch nicht beendet. Dieses Verhalten wurde verursacht, da durch notify() der „falsche“ Thread (ein Verbraucher) geweckt wurde. Alois Schütte AOSD 19 Threads: wait und notify Lösung: notify durch notifyAll ersetzen: class Buffer { private boolean available=false; private int data; public synchronized void put(int x) { while(available) { try { wait(); } catch(InterruptedException e) {} } data = x; available = true; notifyAll(); } public synchronized int get() { while(!available) { try { wait(); } catch(InterruptedException e) {} } available = false; notifyAll(); return data; } } // end Buffer Die Methode notifyAll() ist zu verwenden, wenn mindestens eine der beiden folgenden Situationen zutrifft: • In der Warteschlange befinden sich Threads, mit unterschiedlichen Wartebedingungen (z.B. Puffer leer, Puffer voll). Dann kann bei Verwendung von notify() der „falsche“ Thread geweckt werden. • Durch die Veränderung des Zustands eines Objekts können mehrere Threads weiterlaufen (Wert im Puffer alle wartenden Verbraucher können arbeiten). Alois Schütte AOSD 20 Threads: Deadlocks Synchronisation mittels synchronized verhindert keine Deadlocks: $ cat Deadlock.java public class Deadlock { public static void main(String[] args) { // resource objects to locks on final Object resource1 = ”r1"; final Object resource2 = ”r2"; t1 r1 r2 t2 // first thread - it tries to lock resource1 then resource2 Thread t1 = new Thread() { public void run() { // lock resource1 synchronized(resource1) { System.out.println("Thread 1: locked resource 1"); try { Thread.sleep(50); } // pause... catch (InterruptedException e) {} // attempt to lock resource2 System.out.println("Thread 1: wants resource 2"); synchronized(resource2) { System.out.println("Thread 1: locked resource 2"); } } // synchronized } //run }; // Thread 1 Alois Schütte AOSD 21 Threads: Deadlocks // second thread - it tries to lock resource2 then resource1 Thread t2 = new Thread() { public void run() { // lock resource2 synchronized(resource2) { System.out.println("Thread 2: locked resource 2"); try { Thread.sleep(50); } // pause... catch (InterruptedException e) {} System.out.println("Thread 2: wants resource 1"); synchronized(resource1) { System.out.println("Thread 2: locked resource 1"); } } // synchronized } // run }; // Thread 2 // start threads - should deadlock and program will never exit t1.start(); t2.start(); }// main } $ Alois Schütte AOSD t1 r1 r2 t2 $ java Deadlock Thread 1: locked resource 1 Thread 2: locked resource 2 Thread 1: wants resource 2 Thread 2: wants resource 1 22