FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS K33 THREADS 1 Nebenläufigkeit Unter Nebenläufigkeit versteht man das gleichzeitige oder quasigleichzeitige Ablaufen von Aktivitäten im Rechner. Ersteres ist aber nur mit Hilfe von mehreren Rechnern oder Prozessoren möglich. Diese Aktivitäten bzw. Kontrollflüsse heissen in Java "Programmfäden" oder Threads. Threads gehören zu den fortgeschrittenen Techniken der Programmierung. Die Thread-Programmierung bringt folgende Vorteile mit sich: § einfache Programmierung von Animationen § einfacheres Programm-Design § § unabhängige Probleme können nebeneinander bearbeitet werden § gilt speziell für interaktive Programme schnellere Programmausführung bei echter Parallelität K33_Threads-L, V20 © H. Diethelm Seite 1/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS Hinter der Java-Kulisse sind auch System-Threads aktiv, z.B. § für Garbage-Collection § für Event-Handling (Benutzer-Eingaben) § der implizite main()-Thread 2 Programme, Prozesse und Threads Die Begriffe Programm, Prozess und Thread meinen nicht dasselbe und müssen differenziert betrachtet werden: § Ein Programm bezeichnet eine statische Liste von Anweisungen. § Unter einem Prozess versteht man ein Programm oder Teile eines Programms, die gerade ausgeführt werden; sie sind aktiv. Ein Prozess ist eine aktive Verarbeitungseinheit. § Ein Thread ist eine unabhängige aktive Verarbeitungseinheit, die im gleichen Prozesskontext läuft. Man spricht auch von Coroutinen. Der Prozesskontext (Speicher, I/O) kann von allen Threads benutzt werden (vgl. Synchronisierung). K33_Threads-L, V20 © H. Diethelm Seite 2/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Die meisten Programmiersprachen bieten keine eingebaute Unterstützung für Threads. Threads können dann gegebenenfalls via Betriebssystemaufrufe realisiert werden, falls das Betriebssystem Threads unterstützt. In Java dagegen sind Threads fester Bestandteil der Sprache (vgl. JVM) und nahtlos in das Konzept der Objektorientierung eingebaut. Je nach Betriebssystem kann die Anzahl Threads pro Prozess unterschiedlich sein: In Fällen, wo echte Parallelität nicht möglich ist, simuliert ein Scheduler die Parallelität, indem er den verschiedenen Threads in gewissen Abständen den Prozessor zuteilt und gegebenenfalls wieder entzieht. Im Zusammenhang mit Java sind folgende zwei Scheduler-Strategien von Bedeutung: § reines Priority Scheduling § Priority Scheduling mit Time-Slicing Bei beiden Verfahren handelt es sich um sogenanntes Preemptive-Scheduling, weil die JVM in der Lage ist, einen laufenden Thread zu unterbrechen. Beim ersten Verfahren wird ein Thread so lange wie möglich ausgeführt, d.h. bis er sich selber "zurückzieht" oder bis er von eiK33_Threads-L, V20 © H. Diethelm Seite 3/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS nem "wichtigeren" Thread (vgl. Thread mit höherer Priorität) verdrängt wird. Beim zweiten Verfahren gelangen alle "willigen" Threads mit der höchsten Priorität periodisch für eine bestimmte Zeit zur Ausführung. Dieses Verfahren ist allerdings nichtdeterministisch, weil zu einem bestimmten Zeitpunkt nicht eindeutig ist, welcher Thread gerade ausgeführt wird. Die Java-Spezifikation schreibt nicht zwingend ein bestimmtes Scheduling-Verfahren für eine JVM vor!! 3 Threads in Java 3.1 Mittels der Klasse Thread In Java sind alle Threads Instanzen bzw. Objekte der Klasse java.lang.Thread. Der direkte Weg zu einem Thread führt deshalb über eine Unterklasse von Thread: Thread MyThread run() In der Unterklasse muss die Methode run() überschrieben werden. Die Methode run() beinhaltet die Programmanweisungen für den Thread. Sie ist vergleichbar mit der main()Methode des eigentlichen Java-Programmes. K33_Threads-L, V20 © H. Diethelm Seite 4/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Beispiel Counter1: public class Counter1 extends Thread { private int count = 0; private int inc; public Counter1(int inc) { this.inc = inc; } public void run() { for (int i=0; i<5; i++) { try { System.out.print(count + " "); count += inc; sleep((int)(1000*Math.random())); } catch (InterruptedException e) { System.out.println(e); } } } } Die Klassenmethode sleep() bewirkt, dass der aktuelle Thread für mindestens eine bestimmte Zeit "schlafengelegt" wird; im obigen Beispiel im Mittel für 1000 Millisekunden bzw. 1 Sekunde. Mit anderen Worten, dem Thread wird für mindestens diese Zeit der Prozessor nicht mehr zur Verfügung gestellt. sleep() kann eine checked InterruptedException werfen, weshalb ein Aufruf mit try und catch erforderlich ist. K33_Threads-L, V20 © H. Diethelm Seite 5/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS ACHTUNG: Zeiten alternativ mittels Warteschleifen aktiv zu "verbraten" ist tabu!! In diesem Fall könnte der Prozessor während dem Warten für andere Threads nicht mehr zur Verfügung stehen! public class CounterDemo { public static void main(String args[]) { Counter1 thread1 = new Counter1(1); Counter1 thread2 = new Counter1(-1); thread1.start(); thread2.start(); } } Nach dem Instanzieren gelangt ein Thread-Objekt mit start() zur Ausführung. Seine run()-Methode wird dann in einem neuen Thread abgearbeitet. ACHTUNG: Ein direkter Aufruf von run() führt zu keinem neuen Thread; run() würde in diesem Fall nur vom aufrufenden Thread abgearbeitet! Ein Thread endet mit dem Verlassen bzw. dem Ende der Methode run(). Im Beispiel instanziert und startet der implizite "main-Thread" die zwei weiteren Threads thread1 und thread2. Mit dem Ende der Methode main() endet bereits der "main-Thread"; thread1 und thread2 laufen noch weiter und erzeugen z.B. folgenden Output: 0 0 1 2 -1 3 4 -2 -3 -4 Process Exit... K33_Threads-L, V20 © H. Diethelm Seite 6/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS 3.2 Mittels der Schnittstelleklasse Runnable Nicht immer ist eine Realisierung von Threads direkt via eine Unterklasse von Thread möglich, speziell weil Java die Mehrfachvererbung nicht unterstützt. Für solche Fälle geht's indirekt via die Implementierung der Schnittstellenklasse bzw. des Interfaces Runnable. Die Implementierung von Runnable erzwingt einzig das Vorhandensein einer Methode run(): Runnable run() MyThread run() Ein Objekt einer beliebigen Klasse, welche Runnable implementiert und somit eine run()-Methode zu Verfügung stellt, kann nun als Parameter dem Konstruktor eines eigentlichen ThreadObjektes übergeben werden. K33_Threads-L, V20 © H. Diethelm Seite 7/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS Beispiel Counter2: public class Counter2 implements Runnable { private Thread thread; private int count = 0; private int inc; public Counter2(int inc) { this.inc = inc; } public void start() { if (thread == null) { thread = new Thread(this); thread.start(); } } public void run() { for (int i=0; i<5; i++) { try { System.out.print(count + " "); count += inc; thread.sleep((int)(1000*Math.random())); } catch (InterruptedException e) { System.out.println(e); } } } } K33_Threads-L, V20 © H. Diethelm Seite 8/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Weil Counter2 kein Thread ist, steht z.B. nicht ohnehin eine Methode start() zur Verfügung. Diese wird deshalb explizit implementiert, wobei sie den Startbefehl an das interne Thread-Objekt tread delegiert (vgl. Delegation): Runnable run() Counter2 thread:Thread start() run() Thread 1 start() Threads der Klasse Counter2 können nun analog wie solche der Klasse Counter1 verwendet werden: public class CounterDemo { static public void main(String args[]) { Counter1 thread1 = new Counter1(1); Counter2 thread2 = new Counter2(-1); thread1.start(); thread2.start(); } } Hier nochmals ein Output: 0 0 1 2 -1 3 -2 -3 4 -4 Process Exit... K33_Threads-L, V20 © H. Diethelm Seite 9/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS 4 Animation Threads können als aktive Elemente dafür sorgen, dass Bilder laufen lernen, z.B. "ein springender Ball". Beispiel Bouncer1 (ein erster Versuch!): import java.awt.*; public class Ball1 { private Component c; private Graphics g; private int x=50, y=50, dx=7, dy=2, r=10; private boolean go = true; public Ball1(Component c, Graphics g) { this.c = c; this.g = g; } public void terminate() { go = false; } public void display() { while (go) { g.setColor(Color.white); g.fillOval(x-r, y-r, 2*r, 2*r); if ((x-r+dx) <= 0) dx = -dx; if ((x+r+dx) >= c.getWidth()) dx = -dx; if ((y-r+dy) <= 0) dy = -dy; if ((y+r+dy) >= c.getHeight()) dy = -dy; x += dx; y += dy; g.setColor(Color.black); g.fillOval(x-r, y-r, 2*r, 2*r); try { Thread.sleep(50); } catch (InterruptedException e) { } } c.repaint(); // clears the last ball-drawing } } K33_Threads-L, V20 © H. Diethelm Seite 10/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS import java.awt.*; import java.applet.Applet; import java.awt.event.*; public class Bouncer1 extends Applet implements ActionListener { private Button start, stop; private Ball1 ball; public void init() { start = new Button("Start"); add(start); start.addActionListener(this); stop = new Button("Stop"); add(stop); stop.addActionListener(this); } public void actionPerformed(ActionEvent e) { if (e.getSource() == start) { if (ball == null) { ball = new Ball1(this, getGraphics()); ball.display(); } } if (e.getSource() == stop) { if (ball != null) { ball.terminate(); ball = null; // => frees memory } } } } K33_Threads-L, V20 © H. Diethelm Seite 11/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Leider kann man das Applet mit dem Stop-Button nicht mehr beenden! Der Ball springt dauernd im Fenster umher. Weshalb? § Der Kontrollfluss des gesamten Programms beschränkt sich einzig auf den impliziten main-Thread. § Der Kontrollfluss ist zudem in der whileSchleife der Methode display() "gefangen". § Die Methode actionPerformed() lässt sich somit nicht mehr aktivieren (vgl. Fokus)! K33_Threads-L, V20 © H. Diethelm Seite 12/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Beispiel Bouncer2: import java.awt.*; public class Ball2 extends Thread { private Component c; private Graphics g; private int x=50, y=50, dx=7, dy=2, r=10; private boolean go = true; public Ball2(Component c, Graphics g) { this.c = c; this.g = g; } public void terminate() { go = false; } public void run() { while (go) { g.setColor(Color.white); g.fillOval(x-r, y-r, 2*r, 2*r); if ((x-r+dx) <= 0) dx = -dx; if ((x+r+dx) >= c.getWidth()) dx = -dx; if ((y-r+dy) <= 0) dy = -dy; if ((y+r+dy) >= c.getHeight()) dy = -dy; x += dx; y += dy; g.setColor(Color.black); g.fillOval(x-r, y-r, 2*r, 2*r); try { sleep(50); } catch (InterruptedException e) { } } c.repaint(); } } K33_Threads-L, V20 © H. Diethelm Seite 13/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS import java.awt.*; import java.applet.Applet; import java.awt.event.*; public class Bouncer2 extends Applet implements ActionListener { private Button start, stop; private Ball2 ball; public void init() { start = new Button("Start"); add(start); start.addActionListener(this); stop = new Button("Stop"); add(stop); stop.addActionListener(this); } public void actionPerformed(ActionEvent e) { if (e.getSource() == start) { if (ball == null) { ball = new Ball2(this, getGraphics()); ball.start(); } } if (e.getSource() == stop) { if (ball != null) { ball.terminate(); ball = null; // => frees memory } } } } K33_Threads-L, V20 © H. Diethelm Seite 14/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Jetzt kann man das Applet wie gewünscht mit dem Stop-Button beenden. Weshalb? § Das Programm beinhaltet 2 Threads bzw. 2 Kontrollflüsse, die "parallel" ablaufen. § Der ball-Thread kümmert sich um die Animation; Der main-Thread um die Interaktion mit dem Benutzer. § Während der ball-Thread schläft hat der main-Thread genügend Zeit, die Benutzereingaben zu verarbeiten (vgl. quasiparallel). Zwischen den beiden Threads findet keine eigentliche Interaktion (vgl. Synchronisation oder Kommunikation) statt, weshalb die Programmierung einfach und unkritisch ist. K33_Threads-L, V20 © H. Diethelm Seite 15/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Beispiel Bouncer3: Bouncer2 kann einfach auf beliebig viele Bälle erweitert werden. Jeder Ball wird von einem separaten Thread animiert. import import import import java.awt.*; java.applet.Applet; java.awt.event.*; java.util.*; public class Bouncer3 extends Applet implements ActionListener { private Button start, stop; private Stack stack; public void init() { start = new Button("Start"); add(start); start.addActionListener(this); stop = new Button("Stop"); add(stop); stop.addActionListener(this); stack = new Stack(); } public void actionPerformed(ActionEvent e) { if (e.getSource() == start) { Ball2 ball = new Ball2(this, getGraphics()); ball.start(); stack.push(ball); } if (e.getSource() == stop) { if (!stack.isEmpty()) { Ball2 ball = (Ball2) stack.pop(); ball.terminate(); } } } } K33_Threads-L, V20 © H. Diethelm Seite 16/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS 4.1 Idiom für ein Animations-Applet Unter einem Idiom versteht man ein allgemeines Lösungsmuster. So kann ein allgemeines Animations-Applet ohne Benutzerinteraktion gemäss folgendem Idiom implementiert werden: import java.applet.Applet; import java.awt.*; public class Animationlet extends Applet implements Runnable { private Thread thread; private int delay = 50; private boolean go = true; private int r = 0; private int maxr = 100; public void init() { } // is called first public void start() { // is called second if (thread == null) { thread = new Thread(this); thread.start(); } } K33_Threads-L, V20 © H. Diethelm Seite 17/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS public void stop() { if (thread != null) { go = false; thread = null; } } public void run() { while (go) { repaint(); try { Thread.sleep(delay); } catch (InterruptedException e) { } } } public void paint(Graphics g) { if (r > maxr) r = 0; g.drawOval(150-r, 150-r, 2*r, 2*r); r++; } } Was animiert wohl obiges Applet? § Ein im Radius kontinuierlich wachsender Kreis (r = 0...100). § Wird rmax (100) überschritten, so "zerplatzt" der Kreis (r à 0). § Das ganze "Spiel" beginnt von vorne. K33_Threads-L, V20 © H. Diethelm Seite 18/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS 5 Thread-Steuerung 5.1 Thread-Zustände Einige Befehle ermöglichen es dem Programmierer, den Lebenszyklus von Threads zu steuern. Die eigentliche Thread-Ausführung wird aber direkt von der JVM oder gar dem unterliegenden Betriebssystem kontrolliert. Folgendes Zustandsdiagramm illustriert den Lebenszyklus von Threads: lebend suspendiert aktiv sleep() schlafend Time out start() lauffähig neu new wait() wartend notify() notifyAll() scheduled yield() join() blockiert eingetroffen interrupt() läuft run() beendet tot = null Garbage Collection /throws InterruptedException (Anschliessend wird nur auf die wichtigsten Thread-Zustände eingegangen, die oben grau markiert sind.) Ein Thread befindet sich immer in einem von 3 bzw. 4 Zuständen: neu: Nach dem Instanzieren mit new(). K33_Threads-L, V20 © H. Diethelm Seite 19/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS lebend/aktiv: Nach dem Aufruf von start() ist der Thread lauffähig, d.h. zur Abarbeitung bereit. Die start()-Methode ruft insbesondere die Methode run() des Threads auf. Es ist aber nicht zwingend, dass sie unmittelbar auf dem Prozessor zur Ausführung gelangt (vgl. Thread-Priorität, andere lauffähige Threads)! Der aktive Thread bewirkt mit dem Aufruf von yield(), dass ein anderer Thread mit derselben Priorität und dem Zustand "lauffähig" den Prozessor zur Abarbeitung bekommt. Existiert kein solcher Thread, so zeigt der Aufruf keine Wirkung. Mittels yield() kann erreicht werden, dass der Prozessor auch ohne Time-Slicing abwechslungsweise Threads mit gleicher Priorität zur Verfügung steht. lebend/suspendiert: Nach dem Aufruf von sleep(), wait()oder join() wird ein Thread suspendiert, d.h. er ist vorläufig nicht mehr zur Ausführung bereit. Der Aufruf von sleep() entzieht dem Thread mindestens für eine bestimmte Zeit die Bereitschaft zur Ausführung. Die Methoden wait(), notify() und notifyAll() sind in der Klasse Object deklariert und beeinflussen ebenfalls die Zustände von Threads. Der Aufruf von wait() auf ein bestimmtes Objekt bewirkt, dass der ausführende Thread suspendiert wird. Ein weiterer Thread kann mittels notify() dasselbe Objekt benachrichtigen, so dass ein suspendierter Thread wieder in den Zustand "lauffähig" wechselt. notifyAll() führt dazu, dass sämtliche an diesem Objekt suspendierten Threads wieder den Zustand "lauffähig" annehmen. K33_Threads-L, V20 © H. Diethelm Seite 20/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS tot: Sobald die run()-Methode zu Ende abgearbeitet ist, stirbt der Thread. Ein wiederholter Start ist nicht möglich. Der vom Thread beanspruchte Speicher wird aber erst freigegeben, wenn der Thread nicht mehr referenziert wird und die Garbage Collection ihre Arbeit verrichtet hat. 5.2 Thread-Prioritäten Jeder Thread besitzt eine Priorität. Die Priorität wird mit einem Integer-Wert von 1 bis 10 repräsentiert, wobei 5 der Default-Wert ist. Wird mit new ein neuer Thread erzeugt, so bekommt dieser automatisch die gleiche Priorität zugewiesen, die auch der Thread besitzt, welcher new aufgerufen hat. Mit setPriority() kann aber die Priorität nachträglich geändert werden. Sind mehrere Threads im Zustand "lauffähig", so kommt derjenige mit der grössten Priorität zuerst zur Ausführung. Besitzen mehrere Threads die gleiche Priorität, so wird entweder einer von ihnen zufällig ausgewählt (reines Priority Scheduling) oder alle werden abwechslungsweise ausgeführt (Priority Scheduling mit Time-Slicing). ACHTUNG: Die korrekte Programmausführung sollte nicht von den Thread-Prioritäten abhängig sein! Die Prioritäten sollen höchstens zur Minimierung der Laufzeit optimiert werden! K33_Threads-L, V20 © H. Diethelm Seite 21/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS 6 Kommunikation und Synchronisation Threads sind selten voneinander unabhängig. Im Allgemeinen kommunizieren sie miteinander oder müssen miteinander synchronisiert werden, d.h. sie haben gemeinsame "Berührungspunkte". 6.1 Motivation Im folgenden Beispiel gibt es einen main()-Thread sowie zwei CounterThreads ct1 und ct2. Der main()-Thread erzeugt ein Counter-Objekt counter sowie ct1 und ct2. ct1 und ct2 greifen beide auf counter bzw. auf c zu; zählen also simultan denselben Zähler hoch. public class CounterThread extends Thread { private Counter c; public static void main(String[] args) { Counter counter = new Counter(); CounterThread ct1 = new CounterThread(counter); CounterThread ct2 = new CounterThread(counter); ct1.start(); ct2.start(); } public CounterThread(Counter c) { this.c = c; } public void run() { c.count(); } } K33_Threads-L, V20 © H. Diethelm Seite 22/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS Die Klasse Counter realisiert den eigentlichen Zähler, wobei die Instanzvariable i den aktuellen Zählerstand speichert. Mit Hilfe der Methode count() soll der Zähler genau 100 Mal inkrementiert werden. Während dem Hochzählen erfolgt jeweils die Ausgabe des aktuellen Zählerstandes sowie ein sleep() von durchschnittlich 10 Millisekunden. Die Methode ist bewusst etwas "holprig" implementiert. public class Counter { private int i = 0; public void count() { int limit = i + 100; while (i++ != limit) { System.out.println(i); try { Thread.sleep((int)(10*Math.random())); } catch (InterruptedException e) { } } } } (Versuchen Sie als Übung, das Klassendiagramm der Anwendung aufzuzeichnen!) K33_Threads-L, V20 © H. Diethelm Seite 23/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS Leider verhält sich das so implementierte Programm seltsam. Das wiederholte Starten führte beispielsweise zu folgenden Ausgaben: a) Die beiden CounterThreads zählen nur auf 100 hoch! b) Der Zähler zählt beliebig weit hoch! c) Der Zähler zählt beliebig weit hoch, teilweise sogar fehlerhaft! Was könnte wohl die Ursache für dieses nichtdeterministische Verhalten sein? a) ct2 wird nicht richtig berechnet. Sollte höher zählen als ct1. b) i wird während Auswertung i++ != limit verändert c) i wird vor Ausgabe System.out.println(i) verändert K33_Threads-L, V20 © H. Diethelm Seite 24/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Die Problematik liegt darin, dass beide CounterThreads auf demselben Counter bzw. derselben Instanzvariablen i operieren. Die Instanzvariable i stellt dabei einen gemeinsamen Speicherbereich für beide Threads dar (vgl. Shared Memory): Es ist nicht eindeutig, wie die beiden Threads zeitlich auf die gemeinsame Variable i zugreifen und diese manipulieren! 6.2 Gegenseitiger Ausschluss (mutual exclusion = mutex) Obige Problematik taucht immer dann auf, wenn mehrere Threads auf gemeinsame Betriebsmittel (vgl. Prozesskontext) zugreifen wollen; die Mittel aber verlangen, dass nur 1 Thread zugreift! Beispiele dafür sind: Speicher, Filesystem, Drucker, Netzwerk K33_Threads-L, V20 © H. Diethelm Seite 25/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Folgendes Beispiel illustriert die Problematik anschaulicher. Wir nehmen dazu an, zu Beginn sei a = 0: Die CR (Critical Region) kann von den beiden Threads verschieden durchlaufen werden (vgl. Ausführung auf Stufe Byte-Code): a) Die Variable a besitzt nun welchen Wert? b) Welchen Wert besitzt wiederum die Variable a? K33_Threads-L, V20 © H. Diethelm Seite 26/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Der Programmzustand ist also davon abhängig, wie die Threads abgearbeitet werden (Race Condition)!! Bemerkung: LOAD, ADD und STORE sind sogenannte atomare Anweisungen, die bei ihrer Ausführung nicht unterbrochen werden können. Gefragt sind also Lösungen, die den gegenseitigen Ausschluss bei einer CR garantieren. 6.3 Das Monitor-Konzept bei Java Java realisiert den gegenseitigen Ausschluss mit Hilfe des Monitorkonzeptes (C.A.R. Hoare, P. Hansen). Ein Monitor überwacht eine oder mehrere CRs und sorgt dafür, dass diese nicht von mehreren Threads gleichzeitig benutzt werden. CRs sind Methoden oder seltener Anweisungsblöcke, die mit dem Schlüsselwort synchronized gekennzeichnet sind. Für jedes Objekt, das als CRs gekennzeichnete Methoden enthält, legt Java zur Laufzeit einen Monitor an. Betritt ein Thread während der Abarbeitung eine CR, so hat kein anderer Thread mehr Zugriff auf synchronisierte Methoden desselben Objektes. Er wird beim Versuch des Zugriffs vielmehr blockiert, bis der frühere Thread die CR wieder freigibt. Als CRs gekennzeichnete Anweisungsblöcke können grundsätzlich von einem Monitor eines beliebigen Objektes kontrolliert werden. Deshalb folgt dem Schlüsselwort synchronized eine Referenzvariable, die zum entsprechenden Objekt verweist. Ein Monitor ist also dafür besorgt, dass in seinem Zuständigkeitsbereich nur eine CR aufs Mal betreten werden kann. Eine CR repräsentiert sozusagen 1 "atomare Anweisung". Nicht synchronisierte Methoden und Anweisungsblöcke können selbstverständlich nebeneinander von mehreren Threads benutzt werden. K33_Threads-L, V20 © H. Diethelm Seite 27/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS Auf unser Programmbeispiel übertragen, muss einzig die Methode count() mit dem Modifizierer synchronized ergänzt werden: public class Counter { private int i = 0; public synchronized void count() { int limit = i + 100; while (i++ != limit) { System.out.println(i); try { Thread.sleep((int)(10*Math.random())); } catch (InterruptedException e) { } } } } Die Threads ct1 und ct2 kommen sich jetzt gegenseitig nicht mehr ins Gehege und liefern einen deterministischen Output: Allerdings wird bei dieser Lösung zuerst ct1 vollständig abgearbeitet und erst anschliessend ct2. Die beiden Threads laufen also nicht "verzahnt" ab. K33_Threads-L, V20 © H. Diethelm Seite 28/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS Folgende Lösung ermöglicht, dass während der eine Thread schläft, der andere (falls er nicht auch schläft) weiterzählen kann: public class Counter { private int i = 1; public void count() { for (int m=0; m<100; m++) { synchronized(this) { System.out.println(i); i = i+1; } try { Thread.sleep((int)(10*Math.random())); } catch (InterruptedException e) { } } } } Oben ist nur ein Anweisungsblock mit synchronized gekennzeichnet. Das nachgestellte (this) legt fest, dass der Anweisungsblock vom Monitor des aktuellen Objektes kontrolliert wird. ct1 und ct2 generieren nun parallel den Output: K33_Threads-L, V20 © H. Diethelm Seite 29/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS 6.4 Beispiel "Produzent-Konsument" Im bisherigen Beispiel haben zwei Threads via Shared Memory miteinander kommuniziert. Die beiden Threads liefen aber unabhängig voneinander ab; eine durch die Problemstellung bedingte Synchronisation war nicht erforderlich. Bei folgendem Beispiel ist dies anders: Ein Produzent produziert Daten und übergibt diese einer Warteschlange. Die Warteschlange kann dabei nur eine begrenzte Menge von Daten aufnehmen. Umgekehrt entnimmt ein Konsument die Daten wieder der Warteschlange, um sie anschliessend zu konsumieren. Analog dem letzten Beispiel haben wir es bei der Warteschlange ebenfalls mit Shared Memory bzw. einer CR zu tun. Über die Warteschlange können der Producer- und Consumer-Thread miteinander kommunizieren. Allerdings ist hier zusätzlich eine eigentliche Synchronisation erforderlich. So kann der ProducerThread an die Warteschlange nur Daten übergeben, falls die Warteschlange nicht voll ist. Umgekehrt kann der Consumer-Thread nur Daten entnehmen, wenn die Warteschlange nicht leer ist. Die Zugriffsmethoden put() und get() der Warteschlange müssen dies gegenseitig entsprechend signalisieren (vgl. wait() und notify()). Das beschriebene Produzent-Konsument Beispiel hat stellvertretenden Charakter und kommt häufig in ähnlicher Form vor. K33_Threads-L, V20 © H. Diethelm Seite 30/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS Produzent: public class Producer extends Thread { private BoundedQueue queue; private int n; public Producer(BoundedQueue queue, int n) { this.queue = queue; this.n = n; } public void run() { for (int i=0; i<n; i++) { queue.put(new Integer(i)); System.out.println("produce: " + i); try { sleep((int)(500*Math.random())); } catch (InterruptedException e) { } } } } Ein Producer-Thread produziert also insgesamt n Integer-Objekte und legt diese in der Warteschlange queue ab. Dazwischen legt er sich durchschnittlich für mindestens eine halbe Sekunde schlafen. K33_Threads-L, V20 © H. Diethelm Seite 31/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS Konsument: public class Consumer extends Thread { private BoundedQueue queue; private int n; public Consumer(BoundedQueue queue, int n) { this.queue = queue; this.n = n; } public void run() { for (int i=0; i<n; i++) { Object o = queue.get(); if (o != null) { System.out.println(" consume: " + o); } try { sleep((int)(1000*Math.random())); } catch (InterruptedException e) { } } } } Ein Consumer-Thread entnimmt der Warteschlange queue insgesamt n Objekte und druckt diese auf der Konsole aus. Dazwischen legt er sich durchschnittlich für mindestens eine Sekunde schlafen. K33_Threads-L, V20 © H. Diethelm Seite 32/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Warteschlange, Hauptprogramm: public class BoundedQueue { private private private private private Object[] array; int front = 0; int back = -1; int size = 0; int count = 0; public BoundedQueue(int size) { if (size > 0) { this.size = size; array = new Object[size]; back = size - 1; } } synchronized public boolean isEmpty() { return (count == 0); } synchronized public boolean isFull() { return (count == size); } synchronized public int getCount() { return count; } synchronized public void put(Object o) { if (o != null) { try { while (isFull()) wait(); back++; K33_Threads-L, V20 © H. Diethelm Seite 33/36 FHZ Hochschule für Technik+Architektur Luzern Abteilung Informatik, Fach Programmieren K33 THREADS if (back >= size) back = 0; array[back] = o; count++; notify(); } catch (InterruptedException e) { } } } synchronized public Object get() { Object o = null; try { while (isEmpty()) wait(); o = array[front]; array[front] = null; front++; if (front >= size) front = 0; count--; notify(); } catch (InterruptedException e) { } return o; } public static void main(String args[]) { BoundedQueue queue = new BoundedQueue(5); new Producer(queue, 10).start(); new Consumer(queue, 10).start(); } } K33_Threads-L, V20 © H. Diethelm Seite 34/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Sämtliche Zugriffsmethoden sind synchronisiert, so dass gleichzeitig nur ein Thread Zugriff auf den gemeinsamen Speicherbereich bzw. auf die CR hat. Die Methode put() kann ein beliebiges Objekt in die Warteschlange bzw. im Pufferspeicher ablegen. Falls die Warteschlange voll ist, ist ein Ablegen nicht möglich bzw. der entsprechende Thread muss warten. Der Aufruf von wait() bewirkt, dass der aktuelle Thread in den Zustand "wartend" übergeht. wait() kann nur innerhalb eines synchronisierten Abschnittes für dasjenige Objekt aufgerufen werden (wait() ist eine Methode der Klasse Object!), dessen Monitor diesen Abschnitt kontrolliert. Obwohl der besagte Thread den synchronisierten Abschnitt bzw. die CR noch nicht zu Ende abgearbeitet hat, gibt in diesem Falle der Monitor seine von ihm kontrollierten CRs für andere Threads wieder frei! Erst ein analoger Aufruf von notify() (siehe Methode get()) bewirkt, dass unser Thread oder einer, der bereits früher an derselben Stelle suspendiert wurde, wieder in den Zustand "lauffähig" wechselt. (Mit notifyAll() würden alle an dieser Stelle suspendierten Threads wieder den Zustand "lauffähig" annehmen.) Sobald er effektiv zur Ausführung gelangt, wird er dort fortsetzen, wo er einst unterbrochen wurde. In unserem Fall wird er in der while-Schleife nochmals isFull() aufrufen. Dies ist wichtig, denn es könnte ja sein, dass in der Zwischenzeit andere Threads die Warteschlange bereits wieder gefüllt haben und ein erneutes Warten angesagt ist! Der Aufruf von notify() am "Ende" der Methode put() signalisiert umgekehrt solchen Threads, die innerhalb der Methode get() suspendiert wurden (weil die Warteschlange leer war), dass jetzt mindestens wieder ein Objekt in der Warteschlange drin verfügbar ist. K33_Threads-L, V20 © H. Diethelm Seite 35/36 Abteilung Informatik, Fach Programmieren FHZ Hochschule für Technik+Architektur Luzern K33 THREADS Der Output zeigt, wie Produzent und Konsument zusammenarbeiten und wie letzterer schliesslich alle einst produzierten Daten auf der Konsole ausgibt: Die Warteschlange hat gemäss Instanzierung für 5 Objekte Platz. Tatsächlich sind zu jedem Zeitpunkt nicht mehr wie 5 IntegerObjekte im Puffer! K33_Threads-L, V20 © H. Diethelm Seite 36/36