28
Nebenläufigkeit in Java Maurice Schoenmakers [email protected] Inhaltsverzeichnis 1 Einleitung .................................................................................................................................... 2 2 Prozesse und Threads in Java ..................................................................................................... 2 2.1 Die Klasse Thread ................................................................................................................. 3 2.2 Der aktuelle Thread ................................................................................................................ 3 2.3 Mehrere Threads ................................................................................................................... 4 2.4 Mehrere Threads mit gemeinsamen Variablen ....................................................................... 5 3 Synchronisation paralleler Threads in Java .............................................................................. 7 3.1 Kritischer Bereich .................................................................................................................. 9 3.2 Bewachte kritische Bereiche ................................................................................................ 13 3.3 Das Monitor-Konzept in Java .............................................................................................. 15 3.4 Semaphore ........................................................................................................................... 19 3.4.1 Ganzzahlige Semaphore ............................................................................................... 19 3.4.2 Boolesche Semaphore ................................................................................................... 19 3.5 Verklemmung ...................................................................................................................... 20 3.5.1 Koch-Beispiel .............................................................................................................. 20 3.5.2 Verschachteltes Monitor Problem ................................................................................. 22 4 Zuteilungsstrategien in Java für Threads ................................................................................ 23 4.1 Zeitscheibenstrategie ............................................................................................................ 25 4.2 Sperre mit Warteschlange ..................................................................................................... 26 5 Anmerkungen ............................................................................................................................ 27 6 Literatur ..................................................................................................................................... 28

Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Embed Size (px)

Citation preview

Page 1: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Nebenläufigkeit in Java

Maurice [email protected]−muenchen.de

Inhaltsverzeichnis1 Einleitung .................................................................................................................................... 22 Prozesse und Threads in Java..................................................................................................... 2

2.1 Die Klasse Thread ................................................................................................................. 32.2 Der aktuelle Thread................................................................................................................ 32.3 Mehrere Threads................................................................................................................... 42.4 Mehrere Threads mit gemeinsamen Variablen....................................................................... 5

3 Synchronisation paralleler Threads in Java.............................................................................. 73.1 Kritischer Bereich.................................................................................................................. 93.2 Bewachte kritische Bereiche ................................................................................................ 133.3 Das Monitor−Konzept in Java.............................................................................................. 153.4 Semaphore........................................................................................................................... 19

3.4.1 Ganzzahlige Semaphore ............................................................................................... 193.4.2 Boolesche Semaphore................................................................................................... 19

3.5 Verklemmung...................................................................................................................... 203.5.1 Koch−Beispiel.............................................................................................................. 203.5.2 Verschachteltes Monitor Problem................................................................................. 22

4 Zuteilungsstrategien in Java für Threads................................................................................ 234.1 Zeitscheibenstrategie............................................................................................................ 254.2 Sperre mit Warteschlange..................................................................................................... 26

5 Anmerkungen............................................................................................................................ 276 Literatur ..................................................................................................................................... 28

Page 2: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung undSynchronisation von Nebenläufigkeit zu erläutern. Einfache Grundkentnisse in Java werden dabeivorausgesetzt. Die erläuterten Programmierkonzepte werden im Bezug gesetzt zu denGrundbegriffen zur Programmierung verteilter Systeme aus dem Kapitel 1 des Buches "Informatik,Eine grundlegende Einführung" [Bro98].

2 Prozesse und Threads in Java

Ein Java−Programm besitzt in der Regel eine Klasse mit einer statischen Methode main() .

Bei jedem Start eines Java Programms erzeugt das Betriebsystem einen Prozeß und startet dievirtuelle Maschine (VM). Die virtuelle Maschine erzeugt einen Haupt−Thread (engl.main thread)auch Programmfaden, Kontrollfluß oder leichtgewichtiger Prozeß genannt. Der Haupt−Thread führtdaraufhin die Methode main () aus.

Es wird also zwischen Java−Prozesse und −Threads (threads) unterschieden. Ein Java−Prozeß kann als Realisierung eines Prozesses des Prozeßmodells der Vorlesung gesehenwerden. Ein Java−Thread entspricht einem sequentiellen Teilprozeß. Alle Teilprozesse zusammenformen den Gesamtprozeß.

Eine Methode im Quelltext besteht aus eine Sequenz von Anweisungen. Beim Übersetzen desQuelltextes, wird jede Anweisung in mehrere elementare Teilanweisungen (byte codes) für die VMzerlegt. Ein Java−Threads führt diese einzelnen elementaren Anweisungen einer Methode und dieAnweisungen der darin aufgerufenen Methoden sequentiell aus. Die elementaren Anweisungen sindals Aktionen des Teilprozesses zu sehen.

Bei Applets startet der Internet−Browser ein Java−Programm. Für jedes Applet wird ein eigenerThread erzeugt, der dann die Methode init() ausführt. Mehrere Applets laufen immer gemeinsaminnerhalb des vom Browser erzeugten Prozesses.

Innerhalb eines Java−Programms ist es möglich mit Hilfe der Klasse Thread weiterenebenläufige Threads zu erzeugen. Mehrere Threads laufen parallel ab. Sie können gleichzeitig aufgleiche Objekte und Daten zugreifen. Dadurch kann es zu der, in der Vorlesung erwähntenKonflikte, kommen. Auf die Konflikte und wie sie gelöst werden, wird später detaillierteingegangen.

Mehrere Threads in einem Programm werden gebraucht, um mehrere Aufgaben parallel zuerledigen. Beispiele:

� In einem Textprogramm wird ein Druckauftrag erteilt. Mit dem Editieren kann fortgefahrenwerden. Im Hintergrund wird die Aufbereitung des Textes für den Drucker erledigt.

� In einer Anwendung zum Erfassen und Pflegen von Daten kann in einer Maske eine Suchegestartet werden. In einer anderen Maske können währenddessen Daten weiter editiert werden.

2 / 28

Page 3: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Java Threads und das Betriebssystem

Java−Prozesse werden auf Betriebssystem−Prozesse abgebildet. Sie stellen in Java streng getrennteBereiche dar. So kann ein Thread eines Java−Prozesses nie auf Objekte in anderen Prozessenzugreifen. Eine Kommunikation ist nicht direkt, sondern nur über Verteilungstechnologien wieCORBA oder DCOM, möglich. Diese basieren indirekt auf Netzwerkdiensten des Betriebsystems. Die Java−Threads können von der virtuellen Maschine auf Betriebsystem−Threads abgebildetwerden. Man spricht von einer VM, die native threads unterstützt. Dies ist jedoch nicht immer derFall. Die meisten ersten Unix−Implementierungen der VM verwenden noch ein eigenesBetriebssystem unabhängiges Verfahren, häufig green threads genannt. Dabei werden Threadssimuliert. Diese Simulation benutzt ein speziellen Scheduler, der die Reihenfolge der Ausführungfestlegt , und oft zu einem unerwarteten und manchmal unerwünschten deterministischen Verhalten.(Siehe auch 4. Zuteilungsstrategien in Java)

2.1 Die Klasse Thread

Java bietet eine Klasse Thread an, die einen Thread repräsentiert. Mit Hilfe dieser Klasse könnenThreads erzeugt werden. Es werden hier nur die wichtigsten Methoden besprochen.

2.2 Der aktuelle Thread

Die statische Methode currentThread() liefert den Zugriff auf den aktuellen Thread, der dieMethode gerade ausführt.

Ein einfaches Programm, das den aktuellen Thread am Bildschirm ausgibt:

public class ThreadTest{ public static void main( String[] arguments ) { System.out.println("THREAD:" + Thread.currentThread() ); }}

Am Ende der Methode main() ist auch der Thread und damit der Prozeß beendet.

Ausgabe am Bildschirm:

THREAD:Thread[main,5,main]

Hinter dem Text THREAD: steht, das was die Methode toString() der Klasse Thread zurückliefert. Das sind der Name, die Priorität und die Thread−Gruppe. Auf die Priorität und Gruppe wirdspäter unter 4. genauer eingegangen, sie sind zunächst unwichtig.

3 / 28

Page 4: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

2.3 Mehrere Threads

Jetzt fügen wir eine Klasse Counter hinzu mit einer Methode run(). Die Methode soll die Zahl 0bis einschließlich 1000 ausgeben.Weiterhin erzeugen wir einen zweiten Thread, um zweimal parallel die Methode run()

auszuführen. Die Klasse Counter implementiert die Schnittstelle Runnable um damit die Methode run() fürThreads als die auszuführende Methode anzugeben.

class Counter implements Runnable{ public void run() { Thread thread = Thread.currentThread();

for( long count = 0 ; count <= 1000 ; ++count ) { System.out.println( thread + "COUNT:" + count ); } }}

public class MultipleThreadTest{ public static void main( String[] arguments ) { Counter counter = new Counter();

Thread secondThread = new Thread( counter, "second" );

secondThread.start();

counter.run(); }}

Der Haupt−Thread legt in der Methode main() zunächst ein Objekt counter der Klasse Counter

als Zähler an. Ein zweites Thread Objekt, secondThread , wird erzeugt und dabei wird der Zählermitgegeben. Mit start() startet der Haupt−Thread den zweiten Thread. Dieser ruft intern dierun() Methode des Zählers auf. Der Haupt−Thread läuft parallel weiter und ruft selbst auch dierun() Methode auf.

Die Methode run() wird jetzt parallel von zwei Threads ausgeführt. Jeder Thread arbeitet seineeigene Schleife ab. Der Schleifen−Zähler, bzw. den Zählerstand, count, ist eine lokale Variableder Methode run() und wird auf dem Stack abgelegt. Jeder Thread besitzt sein eigenen Stack.Damit existiert der Zählerstand jetzt zweimal.

Auszug aus einer möglichen Ausgabe:

Thread[second,5,main]COUNT:95Thread[second,5,main]COUNT:96Thread[second,5,main]COUNT:97Thread[main,5,main]COUNT:95Thread[second,5,main]COUNT:98Thread[main,5,main]COUNT:96Thread[second,5,main]COUNT:99

4 / 28

Page 5: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Thread[main,5,main]COUNT:97Thread[second,5,main]COUNT:100Thread[main,5,main]COUNT:98Thread[second,5,main]COUNT:101Thread[main,5,main]COUNT:99Thread[second,5,main]COUNT:102Thread[main,5,main]COUNT:100Thread[second,5,main]COUNT:103Thread[main,5,main]COUNT:101Thread[main,5,main]COUNT:102Thread[main,5,main]COUNT:103

Bemerkung zu den AusgabenDa ein regelmäßiges Abwechseln von Threads für eine Implementierung einer Java VM nichtvorgeschrieben ist, können die Ausgaben immer gleich sequentiell erscheinen. Verwenden Sie fürein weniger deterministisches Verhalten den RoundRobinScheduler aus 4.1 Zeitscheibenstrategieund schreiben Sie in die erste Zeile der Methode main() :

new RoundRobinScheduler(10).start() ;

Bemerkung zur Methode run()

Die Signatur der Methode public void run() wird durch die Schnittstelle Runnable vorgegeben.Diese Schnittstelle wird von der Klasse Thread vorausgesetzt. Man hat grundsätzlich die Wahl,von der Klasse Thread abzuleiten und dessen Methode run() zu überschreiben, oder dieSchnittstelle Runnable zu implementieren und dann beim Erzeugen eines Threads das ausführbareObjekt anzugeben.

Die Methode run() hat keine Parameter, daher müssen benötigte Daten anderweitig weitergereichtwerden. Beispielsweise über Attribute des Objektes, das die Runnable Schnittstelle implementiert.

Bemerkung zu JDK 1.2 Einige Methoden der Klasse Thread werden ab Java 1.2 nicht mehr unterstützt: Die Methode stop() und suspend() gaben keine Sperren frei und konnten somit einfach eineVerklemmung produzieren. Die Methode resume() machte ohne die Methode suspend() keinenSinn mehr. Sperren und Verklemmung werden noch genauer besprochen.

2.4 Mehrere Threads mit gemeinsamen Variablen

Wir ändern die Klasse Counter so ab, daß der Zählerstand count ein Attribut wird.Beide Threads versuchen ihrerseits den Zählerstand zu inkrementieren.Ziel ist, daß beide gemeinsam den Zähler von 0 auf 1000 hochzählen.

5 / 28

Page 6: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

class Counter implements Runnable{ private long count = 0;

public void run() { Thread thread = Thread.currentThread();

for( ; count <= 1000 ; ++count ) { System.out.println( thread + "COUNT:" + count ); } }}

Ausz üge aus einer mögliche Ausgabe:

...Thread[main,5,main]COUNT:44Thread[main,5,main]COUNT:45Thread[main,5,main]COUNT:46Thread[main,5,main]COUNT:47Thread[main,5,main]COUNT:48Thread[main,5,main]COUNT:49Thread[second,5,main]COUNT:49 !!!Thread[main,5,main]COUNT:50Thread[second,5,main]COUNT:51Thread[main,5,main]COUNT:52...Thread[second,5,main]COUNT:995Thread[second,5,main]COUNT:996Thread[second,5,main]COUNT:997Thread[second,5,main]COUNT:998Thread[second,5,main]COUNT:999Thread[second,5,main]COUNT:1000Thread[main,5,main]COUNT:1001 !!!!!

Diese Ausgaben sind, wenn man den Code sieht, nicht direkt plausibel.

Was ist z.B. bei der letzten Ausgabe 1001 passiert ?

Sei count = 999. Jetzt kann folgender Ablauf stattfinden:

� Der Haupt−Thread "main" prüft ob count <= 1000 ist. Dies ist der Fall.� Jetzt kommt Thread "second" an dieser Stelle an und prüft ebenfalls ob count <= 1000 ist.

Dies ist der Fall und count wird inkrementiert und ausgegeben. � Der Haupt−Thread inkrementiert count ebenfalls und gibt ihn aus.

Das Attribut count war inzwischen jedoch bereits von "second" inkrementiert worden, somit istder Wert jetzt 1001 !

Offensichtlich ist es notwendig, beide Threads zu koordinieren. Sie müssen so synchronisiertwerden, daß nur ein Thread das Prüfen der Bedingung und die Inkrementierung atomar, d.h.vollständig ohne Unterbrechung, ausführt.

6 / 28

Page 7: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Wie immer bei fehlender Synchronisation muß der im Beispiel gezeigte Fehler nicht zwingendauftreten und kann lange Zeit völlig unbemerkt bleiben. Je nachdem, wann die Threads aktiv sindbzw. bei einem Prozessor sich abwechseln, ergeben sich unterschiedliche Resultate. Dies macht esextrem schwer in einem fertigen System fehlerhafte Code−Stellen zu finden. Vor allem kann alleindas Suchen eines Fehlers mit Hilfe des Debuggers, den Fehler verschwinden lassen. Durch dieBeobachtung mit dem Debugger wird das Abwechseln der Threads beeinflußt.

3 Synchronisation paralleler Threads in Java

Mehrere Threads können nur über gemeinsame Variablen kommunizieren. Java stellt keine anderenKommunikationsmittel zur Verfügung.

Folgende Konflikte können dabei auftauchen:

� Gleichzeitiges Lesen und Schreiben.Ein Thread liest und ein anderer ändert gerade das Objekt, ist aber noch nicht fertig.

� Gleichzeitiges Schreiben. Mehrere versuchen das Objekt gleichzeitig zu ändern.

Gemeinsam benutzte Objekte sollten aus der Sicht eines Threads einen korrekten konsistentenZustand haben. Der Zustand eines Objektes besteht aus den Werten seiner Attribute und denZuständen der enthaltenen Objekte. Das Ausführen einer Methode kann den Zustand eines Objektes ändern. Eine Methode sollte dasObjekt von einem stabilen korrekten Zustand in einen anderen stabilen korrekten Zustandüberführen. Beim Übergang von einem Zustand in einem anderen Zustand könnenZwischenzustände auftreten, die nach außen nicht sichtbar werden sollen. Sie sind in der Regel vonder Implementierung der Methode abhängig.

Rufen mehrere Threads Methoden gleichzeitig auf, dann kann ohne Synchronisation einZwischenzustand einer Methode den Ablauf einer anderen Methode beeinflussen.

Um Konflikte zwischen Threads zu vermeiden gibt es verschiedene Möglichkeiten.

� Schreiben nicht zulassenEine Klasse wird so definiert, daß eine Änderung seiner Attribute nach dem Erzeugen derObjekte nicht mehr möglich ist. ( engl. immutable class). Dies scheint eine drastischeEinschränkung, kann aber in manchen Fällen durchaus sinnvoll sein. Vor allem, wenn ein Objekteinen Wert darstellt und die Identität nicht wesentlich ist, kann man sich die meist zeitintensiveSynchronisation sparen.

Bei Java können z.B. mehrere Threads problemlos auf ein gleiches Objekt der Klassejava.lang.String zugreifen, da ein String nie mehr geändert werden kann. Jede Methode derKlasse produziert bei einer Änderung des Wertes ein neues String Objekt. Für eine performanteÄnderung eines String steht die Klasse java.lang.StringBuffer zur Verfügung, diese ist abernicht sicher für die gleichzeitige Verwendung durch mehrere Threads. (engl. thread safe)

7 / 28

Page 8: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

� Lesen und Schreiben synchronisierenFür die meisten Objekte jedoch ist die Identität wichtig. Nur eine explizite Synchronisation, dienachfolgend erläutert wird, kann Konflikte vermeiden.

Die SperreJava verknüpft jede Instanz der Klasse java.lang.Object mit einer sogenannten Sperre ( engl.lock). Jedes Objekt ist in Java direkt oder indirekt von der Klasse java.lang.Object abgeleitet,somit ist mit jedem Objekt genau eine Sperre verknüpft.

Diese Sperre entspricht einem Booleschen Semaphor. Eine Sperre wird einem einzigen Threadzugeteilt. Die anderen Threads müssen gegebenenfalls warten, bis der Thread die Sperre wiederfreigibt. Mit diesem gegenseitigen Ausschluß (engl. mutually exclusive lock kurz mutex lock) lassensich Zugriffe auf Objekte so koordinieren, daß Konflikte vermieden werden.

Betrachten wir einen Bereich, in dem gemeinsame Variablen geschrieben oder gelesen werden.Benötigt jeder Thread eine Sperre bevor er diesen Bereich ausführt und gibt er danach die Sperrewieder frei, dann kann zu jedem Zeitpunkt nur ein einziger Thread in dem Bereich aktiv sein. Somitist ein gleichzeitiges Lesen und Scheiben oder ein gleichzeitiges Schreiben der gemeinsamenVariablen in diesem Bereich unmöglich.

In Java müssen alle Bereiche, in dem gemeinsame Variablen geschrieben oder gelesen werden,explizit mit dem Schlüsselwort synchronized versehen werden, um Konflikte zu vermeiden. Sei O ein beliebiger Ausdruck, der ein Java−Objekt liefert, und S eine Menge von Anweisungen, dannist die Syntax:

synchronized( O ){ S}

Erreicht ein Thread die synchronized Anweisung, dann gelten folgende Regeln:

� Der Thread versucht den exklusiven Zugriff auf das Objekt O zu erhalten. � Wenn kein anderer Thread den exklusiven Zugriff hat, gelingt dies. � Besitzt ein anderer Thread den exklusiven Zugriff auf das Objekt, dann wartet der Thread, bis

das Objekt O freigegeben wird. Es können mehrere Threads auf die Freigabe warten. (Siehe auch3. Zuteilungstrategien).

� Erhält ein Thread den exklusiven Zugriff auf O, dann tritt er in den Anweisungsblock ein. � Verläßt der Thread den Anweisungsblock, in dem er den exklusiven Zugriff erhalten hat, dann

gibt er das Objekt O wieder frei.

Die mit einem Objekt verknüpfte Sperre ist sonst nicht direkt zugänglich und kann nur mit demSchlüsselwort synchronized gesetzt und freigegeben werden.

8 / 28

Page 9: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

AusnahmebehandlungAchtung: Wenn eine Ausnahme ( engl. exception) auftritt, die nicht innerhalb einesAnweisungsblocks abgefangen wird, dann wird wie üblich der Anweisungsblock automatischverlassen und dabei auch die Sperre freigegeben. Daher sollte man auch nach einer Ausnahme einenkonsistenten Zustand hinterlassen.

Verschachtelte synchronized AnweisungsblöckeVerschachteln von synchronisierten Anweisungsblöcken ist möglich, auch wenn mehrmals mit demgleichen Objekt synchronisiert wird: Wenn ein synchronisierter Anweisungsblock erreicht wird,kann es also sein, daß der Thread bereits den Zugriff auf das Objekt hat. Dann kann der Threadsofort in dem Anweisungsblock eintreten. Beim Verlassen dieses Anweisungsblocks, wird dasObjekt noch nicht freigegeben. Erst beim Verlassen des äußeren Anweisungsblocks, dort wo er denZugriff ursprünglich erhalten hat, wird das Objekt freigegeben.Damit wird verhindert, daß ein Thread auf sich selbst wartet.

Mit einem synchronized Block wird also gewährleistet, daß nur ein Thread in demAnweisungsblock zu einem Zeitpunkt aktiv sein kann.

3.1 Kritischer BereichWir können jetzt das synchronized Schlüsselwort einsetzen, um den vorher beim Zählerstandgezeigten Fehler zu beheben. Die Prüfung und die Inkrementierung stellen einen sogenanntenkritischen Bereich dar. Dies ist ein Bereich, wo Konflikte zwischen Threads auftreten können.

Wir ändern jetzt den Code so ab, daß die Prüfung und die Inkrementierung nur zusammenausgeführt werden können.

class Counter implements Runnable{ private long count = 0 ;

public void run() { Thread thread = Thread.currentThread(); boolean bContinue = true;

while ( bContinue ) { System.out.println( thread + "COUNT:" + count );

synchronized( this ) { if( count < 1000 ) { ++count; } else bContinue = false; } } }}

9 / 28

Page 10: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Warum wurde this angegeben ?Das Attribut count kann man nicht angeben. Der Typ von count ist long. Dieser Typ ist einsogenannter primitiver Typ, wie auch short , int , byte , char , float und double . Damit ist count

kein Objekt. Allerdings ist count ein Attribut von der Klasse Counter. Folglich gehört zu jedemAttribut namens count genau ein Objekt der Klasse Counter . Daher ist es einfach das Attributcount mit dem zugehörigen Objekt zu synchronisieren. Das Objekt wird in der Methode mit this

angegeben.

Die for− Schleife wurde in eine while− Schleife geändert, damit die Prüfung und dieInkrementierung in einem Block innerhalb der Schleife stattfinden können. Was passiert, wenn der synchronisierte Anweisungsblock die gesamte for− Schleife umfassenwürde ? Warum ist es Problematisch, daß die Ausgabe außerhalb des synchronized Bereichesgemacht wird ?

Synchronisierte Methoden

Wir ändern die Klasse Counter jetzt so, daß die Prüfung und das Inkrementieren in einer eigenenincrement() Methode passieren. Der Rückgabewert gibt an, ob eine Inkrementierung möglich war.

class Counter implements Runnable{ private long count = 0 ;

public void run() { Thread thread = Thread.currentThread(); boolean bContinue = true;

while ( bContinue ) { System.out.println( thread + "COUNT:" + count ); bContinue = increment(); } }

public boolean increment() { synchronized( this ) { if ( count < 1000 ) { ++count; return true; } return false; } }

Jetzt ist die gesamte Implementierung der Methode increment() synchronisiert. Java erlaubt dafürfolgende semantisch äquivalente Schreibweise:

10 / 28

Page 11: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Wir ändern die Methode increment() ab:

public synchronized boolean increment() { if ( count <= 1000 ) { ++count; return true; } return false; }

Dies wird eine synchronisierte Methode genannt. Es wird gewährleistet, daß nur ein Threadgleichzeitig in genau dieser Methode aktiv sein kann. Es wird nicht gewährleistet, daß kein andererThread gleichzeitig in einer anderen Methode aktiv ist. D.h. count kann in einer anderen Methodeweiterhin gleichzeitig geändert werden.

Lesende Methoden müssen synchronisiert werden

Wir führen ein Methode getCount() ein, die den Zählerstand zurück liefert.

public long getCount(){ return count;}

Nehmen wir an, ein Thread ruft irgendwann getCount() auf. Auf den ersten Blick ist nichtersichtlich, daß hier ein Problem vorliegt.

Die Methode getCount() kann ein fehlerhaftes Ergebnis haben und zwar aus zwei folgendenGründen:� getCount() kann ausgeführt werden, während irgendeine andere auch synchronisierte Methode

z.B increment() oder eine andere Methode aus einer Subklasse ausgeführt wird. Diese andereMethode könnte count in ihrem Ablauf zwischenzeitlich auf den Wert −1 setzen und erst amEnde den korrekten Wert zuweisen. Die Methode getCount() könnte dann zwischenzeitlich denunsinnigen Wert −1 liefern.

� Auch wenn keiner den Wert zwischenzeitlich auf −1 setzt, kann getCount() einen falschenWert liefern. Java garantiert, daß alle Operationen auf primitive Typen atomar ausgeführtwerden, außer für double und long . D.h. Die Ausführung von ++count , könnte in 2 Schrittenablaufen. Zwischen diesen beiden Schritten kann ein Threadwechsel auftreten. Der Wert voncount nach dem 1. Schritt ist nicht spezifiziert.

Was wir eigentlich erreichen wollten ist eine immer konsistente Sicht auf Counter von außen.

Daher muß auch getCount() synchronisiert werden:

11 / 28

Page 12: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

public synchronized long getCount(){ return count;}

Damit ist auch klar, warum eine Methode getCount() unerläßlich ist, und count nicht einfachpublic gemacht werden kann. Aus der bisherigen Beschreibung ist ersichtlich, daß, um mehrereThreads zu unterstützen, wesentliche Codeänderungen notwendig waren.

Vollständig synchronisierte Klassen

Sind in einer Klasse alle nicht statischen Methoden über dasselbe Objekt synchronisiert und alleAttribute private, dann kann ein Objekt dieser Klasse nur von jeweils einem Thread bearbeitetwerden. Dies entspricht dem Konzept des Monitors, bei dem nur eine Operation eines Monitorsgleichzeitig ausgeführt werden kann.Die beim Monitor üblichen Benachrichtigungsmechanismen existieren auch in Java. Sie werdenanschließend im Abschnitt 2.3 erläutert.

Synchronisation statischer Methoden

In Java gehört zu jeder Klasse eine, vom System erzeugte Instanz der Klasse java.lang.Class .Diese Instanz beschreibt den Aufbau der Klasse und macht diese zur Laufzeit verfügbar.Der Klassenname gefolgt von ¨.class ¨ , liefert diese Instanz. Wenn eine statische Methode alssynchronized gekennzeichnet wird, dann entspricht dies einer Synchronisierung mit der zu dieserKlasse gehörenden Instanz der Klasse java.lang.Class.

Beispiel:

public class Counter{ private static long maximum = 1000;

public static synchronized long getMaximum() { return maximum; } …}

Die Methode getMaximum() entspricht folgender Implementierung:

public static long getMaximum(){ synchronized( Counter.class ) { return maximum; }}

12 / 28

Page 13: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Synchronisierte Methoden und Vererbung.

Die Synchronisation gehört in Java nur zur Implementierung und wird nicht vererbt ! Daher kannauch eine Schnittstellen−Definition nicht das Schlüsselwort synchronized enthalten.Beim überschreiben einer Methode, darf daher die Synchronisation bei der neuen Implementierungnicht vergessen werden.

3.2 Bewachte kritische Bereiche

Bisher konnte ein Thread nur auf eine einzige Bedingung warten. Und zwar darauf, ob ein Zugriffzur Verfügung steht oder nicht. Manchmal jedoch möchte man komplexere zustandsabhängigeBedingungen formulieren auf deren Erfüllung man wartet.

Man spricht dann von einem bewachten kritischen Bereich. Ein Thread wartet bis eine Bedingungerfüllt ist, bevor er in dem Bereich eintritt. ( engl. guarded suspension)

In der Vorlesung wurde dies durch

await E then S endwait

zum Ausdruck gebracht. Wobei E einen booleschen Ausdruck und S eine Menge von Anweisungenist. Der Ausdruck E stellt den Wächter ( engl.guard ) dar. Sobald E erfüllt ist , wird S exklusiv voneinem einzigen Thread ausgeführt.

Die exklusive Ausführung wird in Java durch das Schlüsselwort synchronized erreicht. DasWarten, hier auf eine Bedingung, wird durch ein Aufruf der Methode wait() eines Objekteseingeleitet:

public class ClassWithGuardedSuspension{ …

private synchronized void awaitConditionE() { while( ! E ) { try { wait (); } catch( InterruptedException ex ){} } }

public void synchronized guardedStatementsS() { awaitConditionE(); S }}

13 / 28

Page 14: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Wozu wird wait() gebraucht ?Würde man statt wait() eine Schleife wie while(! E){ … } verwenden, dann spricht man vonbusy waiting. Der Thread wertet die ganze Zeit, meistens unnötigerweise, die Bedingung aus.Weiterhin wird dabei die Sperre nicht abgegeben. Dadurch kann ein anderer Thread nie an dieReihe kommen, um den Zustand so zu ändern, daß die Bedingung jemals wahr wird !Busy waiting ist folglich zu vermeiden. Ein wartender Thread muß auch benachrichtigt (engl.notified) werden, daß er weiter machen kann.

Daher existieren folgende vom Laufzeitsystem vorgegeben Methoden der Klasse Object: � wait()

� notify() � notifyAll()

Nur wenn ein Thread den exklusiven Zugriff auf ein Objekt hat, kann er dessen wait() , notify()

oder notifyAll() Methode aufrufen. Ansonsten wird ein IllegalMonitorStateException vomLaufzeitsystem erzeugt.

� wait()

� Der aktuelle Thread wird auf wartend gesetzt und in einem mit dem Objekt verknüpften Wartebereich gestellt

� Den exklusiven Zugriff, den der Thread hat, wird freigegeben. Was Voraussetzung dafür ist,daß andere Threads überhaupt notify() oder notifyAll() aufrufen können.

� notify()

� Wenn wartende Threads existieren , dann wird nichtdeterministisch einer ausgewählt unddieser aus dem Wartebereich entfernt.

� Der ausgewählte Thread versucht Zugriff auf das Objekt zu bekommen. Der ausgewählteThread kann den Zugriff nur dann bekommen, wenn der Thread der notify() aufgerufen hatund noch im Besitz des Zugriffs, diesen freigibt und kein anderer Thread den Zugriff erhält.

� Erhält der ausgewählter Thread später den Zugriff, dann setzt er die Ausführung mit dem aufwait() folgendem Code fort.

� notifyAll()

� Verläuft wie notify() mit dem Unterschied, daß alle wartenden Threads aus demWartebereich entfernt werden und wieder versuchen die Sperre zu bekommen.

Es existiert noch die Möglichkeit bei wait() die maximale Wartezeit anzugeben. Ist dieseverstrichen, dann wird automatisch für diesen Thread wie bei notify() verfahren.

Wenn ein Unterbrechung ( engl. interrupt) während eines wait() passiert ( z.B. durch Aufruf derinterrupt() Methode der Klasse Thread), dann wird wie bei notify() verfahren. Die Ausführungwird aber fortgefahren mit dem Werfen einer Ausnahme der Klasse InterruptedException .

Das Ziel ist es mit wait() auf ein Ereignis zu warten. Das Ereignis wird durch das Objekt odereinen Zustand des Objektes repräsentiert. Mit notify() wird dann das Eintreten des Ereignissesmitgeteilt.

14 / 28

Page 15: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Da die Auswahl der Threads beim notify() nichtdeterministisch ist, kann es zum Aushungernkommen. D.h. bestimmte Threads werden nie aus dem Wartbereich ausgewählt. Nur eine selbstprogrammierte Warteschlange kann hier Fairness garantieren. (siehe auch 2.6.2 )

Somit kann jedes Java−Objekt als Signal, wie beim Monitorkonzept in der Vorlesung definiert,verwendet werden.

3.3 Das Monitor−Konzept in Java

Das Monitor−Konzept der Vorlesung läßt sich in Java realisieren durch eine Klasse, in der alleAttribute private sind, alle nicht statischen Methoden als synchronized gekennzeichnet sind unddas Objekt selbst immer als Signal verwendet wird:

� Die Kennzeichnung aller Methoden als synchronized garantiert den gegenseitigen Ausschluß. � Die Verwendung des Objektes selbst als Signal, sorgt für eine Freigabe der Sperre beim wait() ,

und ermöglicht damit die Ausführung der Methoden durch andere Threads.

Bei der Verwendung als Signal, wird für ein Objekt o in der Regel angenommen, daß man beimAufruf von o.wait() auf eine Zustandsänderung von o wartet. Bei o.notify() gibt man dann dieZustandsänderung bekannt. Diese Interpretation ist jedoch nicht zwingend.

Beispiel für einen Monitor unter Verwendung von wait() und notifyAll() :

Wir ändern jetzt increment() , so daß inkrementiert wird, sobald dies möglich ist. DerRückgabewert ist damit überflüssig. Analog zu increment() führen wir eine Methodedecrement() und ein Minimum ein. Die Methode decrement() versucht zu dekrementieren,sobald dies möglich ist. Dann lassen wir einen Thread hochzählen und einen anderen Threadrunterzählen.

public class Counter { private static long maximum = 1000; private static long minimum = 0;

public static synchronized long getMaximum() { return maximum; }

public static synchronized long getMinimum() { return minimum; }

private long count = getMinimum();

private synchronized void awaitIncrementable() { while( !( count < getMaximum() ) ) { try { wait(); }

15 / 28

Page 16: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

catch( InterruptedException e ) {} } }

public synchronized void increment() { awaitIncrementable(); setCount( count +1 ); }

private synchronized void awaitDecrementable() { while( !( count > getMinimum() ) ) { try { wait(); } catch( InterruptedException e ){} } } public synchronized void decrement() { awaitDecrementable(); setCount( count −1 ); }

public synchronized void setCount( long count ) { this.count = count; System.out.println( Thread.currentThread() + "COUNT:" + count ); notifyAll(); }

public synchronized long getCount() { return count; } }

public class CounterTest{ public static void main( String[] arguments ) { final Counter counter = new Counter();

Thread secondThread = new Thread( "second" ) { public void run() { while ( true ) { counter.increment(); } } };

secondThread.start();

while( true ) { counter.decrement(); } }}

16 / 28

Page 17: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Jedesmal wenn count sich ändert, wird setCount und damit notifyAll() aufgerufen. Ein Threadder z.B. bei einem awaitIncrementable wartet, wird dann geweckt. Ist der count < maximum ,dann wird inkrementiert.

Warum wird die Bedingung in einer Schleife geprüft ? Der notifyAll() deutet nur eine Änderung an. Nach einem notifyAll() ist keineswegs dieErfüllung der Bedingung gewährleistet.

Warum wird notifyAll() verwendet ?Der Aufruf von notify() statt notifyAll(), ist nur dann zu empfehlen, wenn es nur eineBedingung gibt und nachher garantiert ist, daß nur ein einziger Thread aktiv werden kann.

Wie kann die Anzahl der notify() Aufrufe und der damit verbundene Synchronisationsaufwandreduziert werden? Hinweis: Wenn der Zustandsraum aufgeteilt wird, ergeben sich nur bestimmteSituationen, wo ein notifyAll() sinnvoll ist.

BemerkungenUnsere Klasse Counter ist ein Monitor im Sinne der Vorlesung.

Wenn in einer synchronisierten Methode ein wait() aufgerufen wird, dann kann die Operationnicht mehr als atomar betrachtet werden! Beim wait() wird die Sperre freigegeben und andereThreads können den Zustand ändern. Daher sollte das Objekt in einen konsistenten Zustand seinbevor wait() aufgerufen wird. Am einfachsten ist dies dadurch zu erreichen, indem vor einemwait() keine Änderungen am Objekt gemacht werden.

Wird der Zustand so geändert, daß ein anderer Thread weitermachen könnte, ruft man notifyAll()

oder notify() auf. Danach sollte der Thread, solange er den Zugriff hat, den Zustand nicht mehrändern. Die geweckten Threads können erst nach der Freigabe der Sperre aktiv werden und nichtschon unmittelbar nach dem notifyAll() Aufruf. Ein geweckter Thread findet dann vielleichteinen Zustand vor, in dem die Bedingung eventuell nicht mehr erfüllt ist. Am einfachsten ist es,wenn ein notify() Aufruf als möglichst letzter Aufruf vor dem Verlassen der Methode verwendetwird.

Eine Nachrichtenschlange in JavaEin Sender und Empfänger kommunizieren über eine Nachrichtenschlange.Der Empfänger soll so lange warten bis ein Objekt vorliegt.

public class MessageQueue { private Vector queue = new Vector(); public synchronized void send( Object object ) { queue.addElement( object ); notifyAll(); }

17 / 28

Page 18: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

public synchronized Object receive() { while( queue.size() == 0 ) { try { wait(); } catch( InterruptedException e ) {} }

Object object = queue.elementAt(0); queue.removeElementAt(0); return object; } } // send a string character by character in an endless looppublic class Sender extends Thread{ private String text; private MessageQueue messageQueue;

public Sender( String text, MessageQueue messageQueue ) { super( "sender of " + text ); this.text = text; this.messageQueue = messageQueue; } public void run() { while ( true ) { for( int i = 0 ; i < text.length() ; ++i ) { messageQueue.send( text.substring( i,i+1 ) ); } } }}

public class MessageQueueTest{ public static void main( String[] arguments ) { MessageQueue messageQueue = new MessageQueue();

Thread sosThread = new Sender( "S.O.S!!!", messageQueue ); sosThread.start(); Thread helloThread = new Sender( "Hello??", messageQueue ); helloThread.start();

while ( true ) { System.out.print( messageQueue.receive() ); }; }}

Beachte die Analogie der Klasse MessageQueue und der Klasse Counter aus dem vorangehendenBeispiel. Erweiteren Sie die Klasse MessageQueue so, daß die Nachrichtenschlange nur einemaximale Anzahl an Elementen beinhalten kann.

18 / 28

Page 19: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

3.4 SemaphoreMit bewachten kritischen Bereiche können, analog zu den Definitionen der Vorlesung, auchSemaphore in Java umgesetzt werden. In der Regel ist eine direkte Verwendung der bestehendenSynchronisationsmechanismen jedoch einfacher.

3.4.1 Ganzzahlige Semaphore Die ganzzahligen Semaphore lassen sich wie folgt realisieren:

public final class CountingSemaphore { private int count = 0;

public CountingSemaphore( int initialCount ) { count = initialCount; }

public synchronized void P() { while( count <= 0 ) { try {

wait(); } catch( InterruptedException ex ) {} } −−count; }

public synchronized void V() { ++count; notify(); }}

Der Aufruf von notify() statt notifyAll() ist ausreichend, da nur ein einziger Thread nach derFreigabe aktiv werden kann: Dieser nimmt sofort die frei gewordene Ressource und die anderenmüssen weiterhin warten.

3.4.2 Boolesche Semaphore

Die boolesche Semaphore werden analog zu den ganzzahligen Semaphoren implementiert:

public final class BooleanSemaphore { private boolean isFree;

public BooleanSemaphore( boolean isFree ) { this.isFree = isFree; }

19 / 28

Page 20: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

public synchronized void P() { while( !isFree ) { try {

wait(); }catch( InterruptedException ex ) {} }

isFree = false; }

public synchronized void V() { isFree = true; notify(); }}

3.5 Verklemmung

3.5.1 Koch−Beispiel

Eine Verklemmung kann leicht auftreten, falls mehrere Threads die gleichen Objekte alsRessourcen in einer verschiedenen Reihenfolge verwenden.

Nehmen wir an, es stehen in einer Küche nur eine Schüssel und nur ein Maßbecher sowie weitereübliche Utensilien zur Verfügung.

Nehmen wir weiterhin an, ein Koch kann mehrere Omelettes machen:

� Er nimmt eine Schüssel,� füllt die Schüssel mit aufgeschlagenen Eiern und rührt diese.� Mit einem Maßbecher mißt er ab, wieviel er für jedes Omelett braucht.� Er backt die einzelnen Omeletts in der Pfanne.

Ein Koch kann auch Kekse machen:

� Er nimmt einen Maßbecher, um die Menge an Mehl zu bestimmen.� Er gibt den Inhalt des Maßbechers in eine Schüssel und rührt die Ingredienzen zusammen.� Anschließend formt er die Kekse und backt sie im Backofen.

Das ganze in pseudo Java−Code:

20 / 28

Page 21: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

class Koch{ static Schuessel schuessel; static Massbecher massbecher; static Mehl mehl; static Ei[] eier; ...

void macheOmeletts( int anzahl ) { synchronized( schuessel ) { schuessel.fuelleMit( brecheEier( eier ) ); schuessel.ruehren();

synchronized( massbecher ) { massbecher.fuelleMit( schuessel.inhalt() ); massbecher.messeAnteil(anzahl); ... } }

} void macheKekse() { synchronized( massbecher ) { massbecher.messe( mehl ); synchronized( schuessel ) {

schuessel.fuelle( massbecher ); schuessel.ruehren();

... } } } }

Nehmen wir an es gibt zwei Köche, Hugo und Fritz. Koch Hugo macht Omeletts und Koch Fritzmacht Kekse.

Wenn zwei Köche sich gleichzeitig in der Küche aufhalten, dann muß eine Koordinationstattfinden. Die allererste selbstverständliche Koordination, die uns einfällt ist, daß die Schüsselnicht von mehreren Köchen gleichzeitig verwendet wird, sondern nur exklusiv von einem Koch.Analoges gilt für den Maßbecher. Ansonsten könnte es passieren, daß die Ingredienzen der Kekseund der Omeletts unkontrolliert vermischt werden. Ein Koch wartet sozusagen als Thread auf eineFreigabe der Schüssel.

Dennoch ist diese Koordination nicht ausreichend, die Reihenfolge ist entscheidend. FolgendeSituation kann auftreten:� Hugo nimmt eine Schüssel, füllt diese mit Eiern.� Fritz nimmt gleichzeitig den Maßbecher und mißt Mehl ab.� Hugo möchte jetzt abmessen wieviel er für jedes Omelett braucht. Er braucht den Maßbecher,

der ist leider noch in Gebrauch.� Fritz möchte jetzt die Schüssel nehmen, um das Mehl rein zu geben, die ist leider noch in

Gebrauch.

21 / 28

Page 22: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Beide warten endlos aufeinander. Ein Verklemmung ist aufgetreten. (engl. deadlock) Mögliche Lösungen sind:

� Es darf nur ein Koch gleichzeitig in die Küche. Damit erhöht man die Sperr−Granularität. Diesverringert die Nebenläufigkeit.

� Oder die Köche reservieren die benötigten Utensilien immer in der gleichen Reihenfolge.Dadurch wird implizit eine Sperren−Hierarchie vorgeschrieben.

� Bei einer Verklemmung wird von einer übergeordneten Instanz die Verklemmung aufgelöst. EinKoch muß die Zwischenergebnisse verwerfen und neu anfangen. Diese Möglichkeit wird vonJava selbst nicht unterstützt.

3.5.2 Verschachteltes Monitor ProblemEine Klasse wie die MessageQueue ist ein Monitor. Beinhaltet ein Monitor ein weiteren Monitorund ist dieser nur über den äußeren Monitor zugänglich, dann kann es ebenfalls leicht zu einerVerklemmung (engl. deadlock) kommen.

Wir ändern jetzt die Klasse MessageQueue so ab, daß das Signal ’’Die Nachrichtenschlange istnicht leer’’, durch eine Boolesche Semaphore dargestellt wird.

public class MessageQueue { private Vector queue = new Vector(); private BooleanSemaphore nonEmpty = new BooleanSemaphore( false ); public synchronized void send( Object object ) { queue.addElement( object ); nonEmpty.V(); } public synchronized Object receive() { nonEmpty.P();

Object object = queue.elementAt(0); queue.removeElementAt(0); return object; } }

Wenn ein Thread receive() aufruft und wenn die Schlange leer ist, dann wird beim wait() ; inder Methode P() nur die Sperre für die Semaphore freigegeben, nicht die Sperre derMessageQueue. Dadurch kann ein anderer Thread nicht in send() eintreten, d.h. nie mehr eineNachricht in die Schlange stellen und den Thread, der in P() wartet, nie mehr benachrichtigen. DerEmpfänger sperrt somit den Sender aus ( engl. lock out )

Das Problem läßt sich lösen indem: � entweder synchronized beim äußeren Objekt entfernt wird, falls dies möglich ist.� Oder indem man dem inneren aggregierten Objekt einen Verweis auf das Äußere mitgibt. Somit

könnte das innere Objekt das äußere Objekt für die Synchronisation verwenden.Dies bedingt aber eine Codeänderung der Klasse BooleanSemaphore !

22 / 28

Page 23: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

4 Zuteilungsstrategien in Java für Threads

Bisher wurde nur von mehreren gleichzeitig aktiven oder wartenden Threads ausgegangen. Threadbenötigen für ihre Ausführung einen Prozessor. Viele Java−Implementierungen unterstützen bishernur einen Prozessor. Bei nur einem Prozessor, kann nur ein Thread aktiv sein. Der Thread wird demProzessor zugeteilt. Man spricht auch von Scheduling (engl.). Die Zuteilungsstrategie beeinflußtwesentlich das Verhalten des Systems bei mehreren Threads.

Um die Zuteilungsstrategien (engl. scheduling strategies) besser zu verstehen, unterscheiden wir diefolgenden Zustände eines Threads:

Initialzustand: Der Thread wurde erzeugt, führt aber noch keine Methode aus. d.h. start() wurde noch nichtaufgerufen.

Lauffähig: Ein Thread geht vom Initialzustand mit start() in den Zustand ¨Lauffähig¨ über.

Aktiv: Ein Thread wird aktiv, wenn er von Javas virtueller Maschine (JVM) dazu aus der Menge derlauffähigen Threads ausgewählt wird. In diesem Zustand führt der Thread tatsächlich Code aus.Der Thread wird vom der JVM dann entweder in den Zustand lauffähig zurück gesetzt oder erkommt in den Zustand ¨Blockiert¨.

Blockiert: Ein Thread ist blockiert, wenn er auf den exklusiven Zugriff auf eines bei synchronized

angegeben Objektes oder auf ein Ereignis mittels wait() wartet. Ist ein Thread blockiert, dann gehter in den Zustand ¨Lauffähig¨ über, falls ein anderer Thread die Sperre freigibt oder falls einanderer Thread mit einem notify() oder notifyAll() das Ereignis meldet und daraufhin die JVMden blockierten Thread auswählt.

Eine andere Möglichkeit blockiert zu werden , ist der Aufruf der Methode void sleep( long

milliseconds ) der Klasse Thread . Wenn die Zeit abgelaufen ist, wird die JVM den Threadwieder in den Zustand ¨Lauffähig¨ setzen. Sperren werden in der Zwischenzeit nicht freigegeben.

Endzustand: Ein Thread ist in seinem Endzustand, falls die run() Methode ausgeführt wurde, oder mit einerAusnahme beendet wurde.

23 / 28

Page 24: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Dieser Automat zeigt die Übergänge zwischen den Zuständen eines Threads

Die entscheidende Frage ist, wie das Java Laufzeitsystem einen Thread zum aktiven Threadbestimmt.

PrioritätenThreads können unterschiedliche Prioritäten haben. Diese werden nur vom Programmiererfestgelegt. Die Java virtuelle Maschine garantiert, daß der Thread mit der höchsten Priorität, immerder aktive Thread ist. D.h. wird ein Thread lauffähig und hat dieser eine höhere Priorität als derderzeit aktive Thread, dann wird der derzeit aktive Thread unterbrochen und in den Zustandlauffähig gesetzt. Der Thread mit der höheren Priorität wird aktiv gesetzt.

Damit besteht in Java die Gefahr des Aushungerns (engl. starvation). Ein Thread niedriger Prioritätläuft Gefahr niemals aktiv zu werden, wenn immer mindestens ein Thread mit einer höherenPriorität lauffähig ist.

Existieren nur lauffähige Threads gleicher Priorität, dann wird nichtdeterministisch ein aktiverThread gewählt. Auch bei notify() wird nichtdeterministisch ein Thread gewählt, der in denZustand lauffähig übergeht. Daher können die Bildschirmausgaben in den Beispielen bei jedemDurchlauf variieren.

Thread−Wechsel

Die virtuelle Maschine muß einen neuen Thread immer nur dann als aktiv auswählen wenn:� ein Thread blockiert, � oder ein Thread höherer Priorität, als der aktive Thread, lauffähig wird.

Folglich:� Ein notify() bedingt nicht notwendigerweise einen Threadwechsel.� Eine Freigabe von Sperren bedingt nicht notwendigerweise einen Threadwechsel.

24 / 28

Aktiv

Blockiert

Intial

Lauffähigstart ()

notify()

notif yAll ()Auswahl durch JVM

Auswahl durch JVM nach notify

Ende

Ende von run() oder unbehandelte Ausnahme

run() , sleep() oder in synchronized() warten auf den Zugriff

Page 25: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Die virtuelle Maschine darf, aber muß niemals, neue Threads aktiv setzen aufgrund von anderenEreignissen. z.B. Unterbrechungen (engl. interrupts) oder Zeiteinheiten. Dies hängt von der jeweiligen Implementierung der VM ab.

Java garantiert insbesondere nicht, daß alle lauffähigen Threads nacheinander für eine festeZeiteinheit (Zeitscheiben) aktiviert werden. (engl. round−robin−strategy)

Ohne eine explizite Angabe, haben alle erzeugten Threads die gleiche Priorität.

Viele Unix−Implementierungen wechseln die Threads nur dann, wenn sie müssen.Die Windows−Implementierungen jedoch meistens auch zu anderen nicht genauer definiertenZeitpunkten. Folglich ist es durchaus möglich, daß bei den Beispielen die Threads nur nacheinanderaktiv werden. Dies ist an sich nicht schlimm, da semantisch die gleiche Aufgabe erledigt wird.Dadurch können aber Synchronisationsfehler unentdeckt bleiben.

Hintergrund−ThreadJava kennt neben normalen Benutzer−Threads (engl.user threads) sogenannte Hintergrund−Threads (engl. daemon threads). In Java endet ein Prozeß sobald alle Benutzer−Threads zu Endesind. Dann werden auch alle Hintergrund−Threads automatisch beendet. Mit den Methoden void

setDaemon( boolean on ) und boolean isDaemon() der Klasse Thread kann diese Eigenschafteines Threads gesetzt und abgefragt werden.Beispielsweise gibt es in einem Java−Prozeß immer ein Hintergrund−Thread, der nicht mehrreferenzierte Objekte aufräumt. (engl. garbage collection thread)

4.1 Zeitscheibenstrategie

Um Abhilfe gegen die sequentielle Ausführung der Threads zu schaffen und das Verhalten derTestprogrammen besser zu illustrieren, können wir eine Zeitscheibenstrategie leichtimplementieren.

class RoundRobinScheduler extends Thread{ int timeslice; public RoundRobinScheduler( int timeslice ) { this.timeslice = timeslice; setPriority( Thread.MAX_PRIORITY ); setDaemon(true); }

public void run() { while( true ) { try { sleep( timeslice ); } catch( Exception e ){} } }

}

25 / 28

Page 26: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Jedesmal, wenn dieser Thread lauffähig wird, wird er aktiviert, da er die höchste Priorität hat.Anschließend blockiert er mit sleep() für einige Millisekunden. Ein anderer Thread kommt an dieReihe usw.

Eine Zeitscheiben−Strategie alleine garantiert nicht, daß alle Threads gleichmäßig an die Reihekommen.

Nehmen wir unsere Klasse Counter mit zwei Threads gleicher Priorität; folgender Ablauf istdenkbar und um so wahrscheinlicher, je mehr Threads das gleiche Counter Objekt verwenden:Beide Threads arbeiten in einer Schleife und der Haupt−Thread hat den Zugriff auf den Zähler. � Die Zeitscheibe ist vorbei, ein Threadwechsel wird erzwungen.� Der zweite Thread wird aktiv, wird jedoch gleich blockiert, da der Haupt−Thread den Zugriff

hat. � Der Haupt−Thread wird wieder aktiv. Der Zugriff wird freigegeben.� Der zweite Thread wird lauffähig aber nicht automatisch aktiv. � Der Haupt−Thread erhält wieder den Zugriff. � Die Zeitscheibe ist vorbei, ein Threadwechsel wird erzwungen. � Der zweiter Thread wird jetzt aktiv aber blockiert sofort wieder. � usw...

Nur wenn eine Unterbrechung stattfindet während der Haupt−Thread den Zugriff freigegeben hat,dann kann der zweite Thread an die Reihe kommen und den Zugriff erhalten.

Erst eine Warteschlange bei der Vergabe des Zugriffs würde Abhilfe schaffen.D.h. Die Wartenden reihen sich ein. Nur der Erste in der Schlange kann den Zugriff nach einerFreigabe erhalten.

4.2 Sperre mit Warteschlange

Im folgenden implementieren wir eine Sperr, die eine Warteschlange beinhaltet mit den Standard−Java−Synchronisationsmechanismen. Ein Thread soll warten, bis er als erster in der Warteschlangesteht.Die Schnittstelle lautet:

interface QueuedLock{ aquire() release();}

Damit läßt sich

synchronized( o ) { ... }

26 / 28

Page 27: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

durch

o.lock.aquire(); { ... } o.lock.release();

ersetzen.

Wobei lock ein öffentliches Attribut von o ist. Das Attribut lock ist dann vom Typ QueuedLock .

Achtung: Eine Schachtelung von o.lock.aquire(){...}o.lock.release(); Bereiche, ohnesich selbst aus zu sperren, wie bei synchronized , sollte möglich sein.

Mit dem Standard−Mechanismus synchronized muß der Thread immer warten, falls er denZugriff nicht enthalten kann. Manchmal gäbe es aber die Möglichkeit eine Alternative auszuführen.Erweitern Sie QueuedLock um eine Methode boolean tryAquire().

5 Anmerkungen

Sicherheit

Threads werden von der virtuellen Maschine hierarchisch gruppiert. Zugang zu dieser Strukturbietet die Klasse ThreadGroup . Alle Applets werden in einem Prozeß ausgeführt. Jedes Applet hateinen eigenen ThreadGroup . Applets sollten nicht die Threads von anderen Applets beeinflussenkönnen, es sei denn, es wird explizit erlaubt. Daher werden Aufrufe für Threads aus einer anderenThreadGroup als der eigenen geprüft. Der Sicherheitsmanager, d.h. die Instanz der KlasseSecurityManager, wird jedesmal gefragt, ob der Aufruf erlaubt ist.

Richtlinien

Die Programmierung für mehrere Threads ist nicht trivial. Nachträglich sequentiellen Code fürmehrere Threads tauglich (engl. thread safe) zu machen, liefert nur selten befriedigende Ergebnisse.

Vor allem die Strategie der Sperrenvergabe ist vorher zu überlegen:

� Die Sperren−Granularität sollte so grob wie möglich gewählt werden. Dies schränkt zwar die Nebenläufigkeit ein, verringert jedoch die Anzahl derKonfliktsituationen. Solange der Ablauf fair ist entsteht auch ein sinnvolles Ergebnis. Es istimmer abzuwägen, ob nur das Endergebnis zählt, oder ob auch Zwischenergebnisse wichtig sind.Wenn nur das Endergebnis zählt, ist eine geringere Nebenläufigkeit tragbar. Da die meistenvirtuellen Maschinen bisher nur einen Prozessor unterstützen, verlangsamt ein häufiger Wechselder Threads die Berechnung eher.Nur wenn Zwischenergebnisse gebraucht werden, sollte die Granularität verfeinert werden.

� Die Anzahl der gemeinsamen Variablen sollte so klein wie möglich bleiben. Z.B. bei der Druckaufbereitung in einem eigenem Thread ist es erheblich einfacher den gesamtenText im Speicher zu kopieren und dann zu formatieren, als die Zugriffe auf dem Text zusynchronisieren.

27 / 28

Page 28: Nebenläufigkeit in · PDF file1 Einleitung Ziel dieser Zusammenfassung ist es, die Programmierkonzepte in Java zur Erzeugung und Synchronisation von Nebenläufigkeit zu erläutern

Fehler

Ein unbedachter Einsatz von Nebenläufigkeit in Java führt auf eine tückische Fehlerquelle. Fehlertauchen in bereits getesteten Programmen unvermittelt, je nach Javaimplementierung, Rechner,Ablaufgeschwindigkeit, Betriebssystem usw. auf. Die Fehlersuche ist aufwendig. die Fehler sindschwer reproduzierbar. Eine Suche mit einem Debugger kann den Ablauf stark beeinflussen.Dadurch können Fehler "verschwinden". Auch die Ausgabe von Meldungen kann dieSynchronisation beeinflussen.

Wiederverwendung

Synchronisation bedeutet ein erhebliches mehr an Code bzw. Code−Änderungen.Synchronisations−Code legt eine Verwendungsart fest. Z.B. kann in einer Methode gewartetwerden, bis eine Bedingung eintritt oder einfach eine Fehlermeldung zurückgeben werden. BeideVerhaltensweisen können je nach Kontext sinnvoll sein.

Es kann sinnvoll sein, den Synchronisations−Code und den funktionalen Kern zu trennen. Der Synchronisations−Code wird dann in eine Adapter−Klasse verlagert, die den funktionalen Kernaufruft. Den funktionalen Kern kann man dann auch in einem sequentiellen Kontext einsetzen, woeine Synchronisation nicht erwünscht ist. Denn Synchronisations−Code beansprucht aucherhebliche Rechenzeit.

6 Literatur

[Bro98] M. Broy, Informatik Eine grundlegende Einführung Band 2, Springer Verlag, 1998[Dou97] Doug Lea, Concurrent Programming in Java, Design Principles and Patterns, Addison

Wesley, 1997[LiF97] Tim Lindholm & Frank Yellin, The Java Virtual Maschine Specification, Addision

Wesley, 1997[ScH97] Scott Oaks & Henry Wong, Java Threads, O´Reilly & Associates, 1997

28 / 28