Threads in Java Wiederholung der BS Grundlagen Alois Schütte AOSD1
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(): Alois Schütte AOSD2 $ cat MyThread.javaMyThread.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(); } } $
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: Alois Schütte AOSD3 $ 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 "; }
Threads: Erzeugen von Threads Alois Schütte AOSD4 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. -> Wiederholung Interfaces
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: Alois Schütte AOSD5 $ 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(); } $
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 AOSD6
Threads: Abarbeitungsreihenfolge Werden mehrere Threads erzeugt, so ist die Ausführungsreihenfolge nicht vorhersehbar! Alois Schütte AOSD7 $ 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(); } $ $ 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)
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: Alois Schütte AOSD8 $ 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; }
Threads: Synchronisation - > Problem des gemeinsamen Zugriffs Das Programm mit einem Thread funktioniert problemlos, also ist die Klasse offensichtlich korrekt implementiert, oder? Alois Schütte AOSD9 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(); } $ $ java Even1 result: 2 result: 4 result: 6 result: 8 result: 10 result: 12 result: 14 result: 16 result: 18 result: 20 $
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? Alois Schütte AOSD10 public class Even2 extends Thread { private Even e; public Even2(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(); Even2 t1 = new Even2(e); Even2 t2 = new Even2(e); t1.start(); t2.start(); } $ $ java Even2 result: 3 result: 4 result: 7 result: 8 result: 11 result: 12 result: 15 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 $
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). Alois Schütte AOSD11 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. Thread A Thread B Sperre Objekt
Threads: Synchronisation -> Synchronized Methoden und Blöcke Somit nun die korrekte Implementierung der Klasse Even: Alois Schütte AOSD12 $ 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; } … $ 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 $
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 AOSD13
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 AOSD14
Threads: wait und notify Alois Schütte AOSD15 class X { synchronized void w() { before();// some actons wait(); // Thread.wait after(); // some actions } synchronized void n() { notify();// Thread.notify } void before {...} void after {...} } begin x.w() acuire lock before(); wait(); release lock enter wait set begin x.w() blocks acuire lock before(); wait(); release lock enter wait set T1 T1 T2 begin x.n() wait for lock aquire lock notify() release lock exit wait set wait lor lock aquire lock after() release lock exit wait set wait lor lock aquire lock after() release lock T2T3 waiting set
Threads: wait und notify Alois Schütte AOSD16 Probleme mit wait() und notify() entstehen, wenn mehrere Threads in der Warteschlange stehen und der falsche Thread geweckt wird. Dies wird am Erzeuger- Verbraucher Problem demonstriert. consumer producer buffer put() get() 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(); } public synchronized int get() { while(!available) { try { wait(); } catch(InterruptedException e) {} } available = false; notify(); return data; } } // end Buffer
Threads: wait und notify Alois Schütte AOSD17 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); }
Threads: wait und notify Alois Schütte AOSD18 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.
Threads: wait und notify Alois Schütte AOSD19 $ 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 Nun werden mehrere Erzeuger und Verbraucher gestartet. 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.
Threads: wait und notify Alois Schütte AOSD20 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).
Threads: Deadlocks Alois Schütte AOSD21 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"; // 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 t1 t2 r1 r2
Threads: Deadlocks Alois Schütte AOSD22 // 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 } $ $ java Deadlock Thread 1: locked resource 1 Thread 2: locked resource 2 Thread 1: wants resource 2 Thread 2: wants resource 1 t1 t2 r1 r2