Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 
Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Eigene Klassen schreiben
6 Objektorientierte Beziehungsfragen
7 Ausnahmen müssen sein
8 Äußere.innere Klassen
9 Besondere Typen der Java SE
10 Generics<T>
11 Lambda-Ausdrücke und funktionale Programmierung
12 Architektur, Design und angewandte Objektorientierung
13 Komponenten, JavaBeans und Module
14 Die Klassenbibliothek
15 Einführung in die nebenläufige Programmierung
16 Einführung in Datenstrukturen und Algorithmen
17 Einführung in grafische Oberflächen
18 Einführung in Dateien und Datenströme
19 Einführung ins Datenbankmanagement mit JDBC
20 Einführung in <XML>
21 Testen mit JUnit
22 Bits und Bytes und Mathematisches
23 Die Werkzeuge des JDK
A Java SE-Paketübersicht
Stichwortverzeichnis


Download:

- Beispielprogramme, ca. 35,4 MB


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 15 Einführung in die nebenläufige Programmierung
Pfeil 15.1 Nebenläufigkeit und Parallelität
Pfeil 15.1.1 Multitasking, Prozesse, Threads
Pfeil 15.1.2 Threads und Prozesse
Pfeil 15.1.3 Wie nebenläufige Programme die Geschwindigkeit steigern können
Pfeil 15.1.4 Was Java für Nebenläufigkeit alles bietet
Pfeil 15.2 Threads erzeugen
Pfeil 15.2.1 Threads über die Schnittstelle Runnable implementieren
Pfeil 15.2.2 Thread mit Runnable starten
Pfeil 15.2.3 Die Klasse Thread erweitern
Pfeil 15.3 Thread-Eigenschaften und Zustände
Pfeil 15.3.1 Der Name eines Threads
Pfeil 15.3.2 Wer bin ich?
Pfeil 15.3.3 Schläfer gesucht
Pfeil 15.3.4 Mit yield() auf Rechenzeit verzichten
Pfeil 15.3.5 Der Thread als Dämon
Pfeil 15.3.6 Freiheit für den Thread – das Ende
Pfeil 15.3.7 Einen Thread höflich mit Interrupt beenden
Pfeil 15.3.8 UncaughtExceptionHandler für unbehandelte Ausnahmen
Pfeil 15.4 Der Ausführer (Executor) kommt
Pfeil 15.4.1 Die Schnittstelle Executor
Pfeil 15.4.2 Glücklich in der Gruppe – die Thread-Pools
Pfeil 15.4.3 Threads mit Rückgabe über Callable
Pfeil 15.4.4 Mehrere Callables abarbeiten
Pfeil 15.4.5 ScheduledExecutorService für wiederholende Ausgaben und Zeitsteuerungen nutzen
Pfeil 15.5 Synchronisation über kritische Abschnitte
Pfeil 15.5.1 Gemeinsam genutzte Daten
Pfeil 15.5.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
Pfeil 15.5.3 Punkte nebenläufig initialisieren
Pfeil 15.5.4 Kritische Abschnitte schützen
Pfeil 15.5.5 Kritische Abschnitte mit ReentrantLock schützen
Pfeil 15.5.6 Synchronisieren mit synchronized
Pfeil 15.5.7 Mit synchronized synchronisierte Blöcke
Pfeil 15.5.8 Dann machen wir doch gleich alles synchronisiert!
Pfeil 15.5.9 Lock-Freigabe im Fall von Exceptions
Pfeil 15.5.10 Deadlocks
Pfeil 15.6 Zum Weiterlesen
 

Zum Seitenanfang

15.5Synchronisation über kritische Abschnitte Zur vorigen ÜberschriftZur nächsten Überschrift

Wenn Threads in Java ein eigenständiges Leben führen, ist dieser Lebensstil nicht immer unproblematisch für andere Threads, insbesondere beim Zugriff auf gemeinsam genutzte Ressourcen. In den folgenden Abschnitten erfahren wir mehr über gemeinsam genutzte Daten und Schutzmaßnahmen beim konkurrierenden Zugriff durch mehrere Threads.

 

Zum Seitenanfang

15.5.1Gemeinsam genutzte Daten Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Thread besitzt zum einen seine eigenen Variablen, etwa die Objektvariablen, kann aber auch statische Variablen nutzen, wie das folgende Beispiel zeigt:

class T extends Thread {

static int result;

public void run() { ... }

}

In diesem Fall können verschiedene Exemplare der Klasse T, die jeweils einen Thread bilden, Daten austauschen, indem sie die Informationen in result ablegen oder daraus entnehmen. Threads können aber auch an einer zentralen Stelle eine Datenstruktur erfragen und dort Informationen entnehmen oder Zugriff auf gemeinsame Objekte über eine Referenz bekommen. Es gibt also viele Möglichkeiten, wie Threads – und damit potenziell parallel ablaufende Aktivitäten – Daten austauschen können.

 

Zum Seitenanfang

15.5.2Probleme beim gemeinsamen Zugriff und kritische Abschnitte Zur vorigen ÜberschriftZur nächsten Überschrift

Da Threads ihre eigenen Daten verwalten – sie haben alle eigene lokale Variablen und einen Stack –, kommen sie sich gegenseitig nicht in die Quere. Auch wenn mehrere Threads gemeinsame Daten nur lesen, ist das unbedenklich; Schreiboperationen sind jedoch kritisch. Wenn sich zehn Nutzer einen Drucker teilen, der die Ausdrucke nicht als unteilbare Einheit bündelt, lässt sich leicht ausmalen, wie das Ergebnis aussieht. Seiten, Zeilen oder gar einzelne Zeichen aus verschiedenen Druckaufträgen werden bunt gemischt ausgedruckt.

Die Probleme haben ihren Ursprung in der Art und Weise, wie die Threads umgeschaltet werden. Der Scheduler unterbricht zu einem uns unbekannten Zeitpunkt die Abarbeitung eines Threads und lässt den nächsten arbeiten. Wenn nun der erste Thread gerade Programmzeilen abarbeitet, die zusammengehören, und der zweite Thread beginnt, nebenläufig auf diesen Daten zu arbeiten, so ist der Ärger vorprogrammiert. Wir müssen also Folgendes ausdrücken können: »Wenn ich den Job mache, dann möchte ich der einzige sein, der die Ressource – etwa einen Drucker – nutzt.« Erst nachdem der Drucker den Auftrag eines Benutzers fertiggestellt hat, darf er den nächsten in Angriff nehmen.

Kritische Abschnitte

Zusammenhängende Programmblöcke, denen während der Ausführung von einem Thread kein anderer Thread »reinwurschteln« sollte und die daher besonders geschützt werden müssen, nennen sich kritische Abschnitte. Wenn lediglich ein Thread den Programmteil abarbeitet, dann nennen wir dies gegenseitigen Ausschluss oder atomar. Wir könnten das etwas lockerer sehen, wenn wir wüssten, dass innerhalb der Programmblöcke nur von den Daten gelesen wird. Sobald aber nur ein Thread Änderungen vornehmen möchte, ist ein Schutz nötig. Denn arbeitet ein Programm bei nebenläufigen Threads falsch, ist es nicht threadsicher (engl. thread-safe).

Wir werden uns nun Beispiele für kritische Abschnitte anschauen und dann sehen, wie wir sie in Java realisieren können.

Nicht kritische Abschnitte

Wenn mehrere Threads auf das gleiche Programmstück zugreifen, muss das nicht zwangsläufig zu einem Problem führen, und Thread-Sicherheit ist immer gegeben. Immutable Objekte – nehmen wir an, ein Konstruktor belegt einmalig die Zustände – sind automatisch threadsicher, da es keine Schreibzugriffe gibt und bei Lesezugriffen nichts schiefgehen kann. Immutable Klassen wie String oder Wrapper-Klassen kommen daher ohne Synchronisierung aus.

Das Gleiche gilt für Methoden, die keine Objekteigenschaften verändern. Da jeder Thread seine threadeigenen Variablen besitzt – jeder Thread hat einen eigenen Stack –, können lokale Variablen, auch Parametervariablen, beliebig gelesen und geschrieben werden. Wenn zum Beispiel zwei Threads die folgende statische Utility-Methode aufrufen, ist das kein Problem:

public static String reverse( String s ) {

return new StringBuilder( s ).reverse().toString();

}

Jeder Thread wird eine eigene Variablenbelegung für s haben und ein temporäres Objekt vom Typ StringBuilder referenzieren.

Threadsichere und nicht threadsichere Klassen der Java-Bibliothek

Es gibt in Java viele Klassen, die nicht threadsicher sind – das ist sogar der Standard. So sind etwa alle Format-Klassen, wie MessageFormat, NumberFormat, DecimalFormat, ChoiceFormat, DateFormat und SimpleDateFormat nicht für den nebenläufigen Zugriff gemacht. In der Regel steht das in der Javadoc, etwa bei DateFormat:

»Synchronization. Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.«

Wer also Objekte nebenläufig verwendet, der sollte immer in der Java-API-Dokumentation nachschlagen, ob es dort einen Hinweis gibt, ob die Objekte überhaupt threadsicher sind.

In einigen wenigen Fällen haben Entwickler die Wahl zwischen threadsicheren und nicht threadsicheren Klassen im JDK:

Nicht threadsicher

Threadsicher

StringBuilder

StringBuffer

ArrayList

Vector

HashMap

Hashtable

Tabelle 15.3Threadsichere und nicht threadsichere Klassen

Obwohl es die Auswahl bei den Datenstrukturen im Prinzip gibt, werden Vector und Hashtable dennoch nicht verwendet.

 

Zum Seitenanfang

15.5.3Punkte nebenläufig initialisieren Zur vorigen ÜberschriftZur nächsten Überschrift

Nehmen wir an, ein Thread T1 möchte ein Point-Objekt p mit den Werten (1,1) belegen und ein zweiter Thread T2 möchte eine Belegung mit den Werten (2,2) durchführen.

Thread T1

Thread T2

p.x = 1;

p.y = 1;

p.x = 2;

p.y = 2;

Tabelle 15.4Zwei Threads belegen beide den Punkt p.

Beide Threads können natürlich bei einem 2-Kern-Prozessor parallel arbeiten, aber da sie auf gemeinsame Variablen zugreifen, ist der Zugriff auf x bzw. y von p trotzdem sequenziell. Um es nicht allzu kompliziert zu machen, vereinfachen wir unser Ausführungsmodell so, dass wir zwar zwei Threads laufen haben, aber nur jeweils einer ausgeführt wird. Dann ist es möglich, dass T1 mit der Arbeit beginnt und x = 1 setzt. Da der Thread-Scheduler einen Thread jederzeit unterbrechen kann, kann nun T2 an die Reihe kommen, der x = 2 und y = 2 setzt. Wird dann T1 wieder Rechenzeit zugeteilt, darf T1 an der Stelle weitermachen, wo er aufgehört hat, und y = 1 folgt. In einer Tabelle ist das Ergebnis noch besser zu sehen:

Thread T1

Thread T2

x/y

p.x = 1;

1/0

p.x = 2;

2/0

p.y = 2;

2/2

p.y = 1;

2/1

Tabelle 15.5Mögliche sequenzielle Abarbeitung der Punktbelegung

Wir erkennen das nicht beabsichtigte Ergebnis (2,1), es könnte aber auch (1,2) sein, wenn wir das gleiche Szenario beginnend mit T2 durchführen. Je nach zuerst abgearbeitetem Thread wäre jedoch nur (1,1) oder (2,2) korrekt. Die Threads sollen ihre Arbeit aber atomar erledigen, denn die Zuweisung bildet einen kritischen Abschnitt, der geschützt werden muss. Standardmäßig sind die zwei Zuweisungen nichtatomare Operationen und können unterbrochen werden. Um dies an einem Beispiel zu zeigen, sollen zwei Threads ein Point-Objekt verändern. Die Threads belegen x und y immer gleich, und immer dann, wenn sich die Koordinaten unterscheiden, soll es eine Meldung geben:

Listing 15.16com/tutego/insel/thread/concurrent/ParallelPointInit.java, main()

final Point p = new Point();



Runnable r = new Runnable() {

@Override public void run() {

int x = (int)(Math.random() * 1000), y = x;



while ( true ) {

p.x = x; p.y = y; // *



int xc = p.x, yc = p.y; // *



if ( xc != yc )

System.out.println( "Aha: x=" + xc + ", y=" + yc );

}

}

};



new Thread( r ).start();

new Thread( r ).start();

Die interessanten Zeilen sind mit * markiert. p.x = x; p.y = y; belegt die Koordinaten neu, und int xc = p.x, yc = p.y; liest die Koordinaten erneut aus. Würden Belegung und Auslesen in einem Rutsch passieren, dürfte überhaupt keine unterschiedliche Belegung von x und y zu finden sein. Doch das Beispiel zeigt es anders:

Aha: x=58, y=116

Aha: x=116, y=58

Aha: x=58, y=116

Aha: x=58, y=116

...

Was wir mit den nebenläufigen Punktinitialisierungen vor uns haben, sind Effekte, die von den Ausführungszeiten der einzelnen Operationen abhängen. In Abhängigkeit von dem Ort der Unterbrechung wird ein fehlerhaftes Verhalten produziert. Dieses Szenario nennt sich im Englischen race condition bzw. race hazard (zu Deutsch auch Wettlaufsituation).

 

Zum Seitenanfang

15.5.4Kritische Abschnitte schützen Zur vorigen ÜberschriftZur nächsten Überschrift

Beginnen wir mit einem anschaulichen Alltagsbeispiel. Gehen wir aufs Klo, schließen wir die Tür hinter uns. Möchte jemand anderes auf die Toilette, muss er warten. Vielleicht kommen noch mehrere dazu, die müssen dann auch warten, und eine Warteschlage bildet sich. Dass die Toilette besetzt ist, signalisiert die abgeschlossene Tür. Jeder Wartende muss so lange vor dem Klo ausharren, bis das Schloss geöffnet wird, selbst wenn der auf der Toilette Sitzende nach einer langen Nacht einnicken sollte.

Wie übertragen wir das auf Java? Wenn die Laufzeitumgebung nur einen Thread in einen Block lassen soll, ist ein Monitor[ 231 ](Der Begriff geht auf C. A. R. Hoare zurück, der in seinem Aufsatz »Communicating Sequential Processes« von 1978 erstmals dieses Konzept veröffentlichte. ) nötig. Ein Monitor wird mithilfe eines Locks (zu Deutsch Schloss) realisiert, den ein Thread öffnet oder schließt. Tritt ein Thread in den kritischen Abschnitt ein, muss Programmcode wie eine Tür abgeschlossen werden (engl. lock). Erst wenn der Abschnitt durchlaufen wurde, darf die Tür wieder aufgeschlossen werden (engl. unlock), und ein anderer Thread kann den Abschnitt betreten.

[»]Hinweis

Ein anderes Wort für Lock ist Mutex (engl. mutual exclusion, also »gegenseitiger Ausschluss«). Der Begriff Monitor wird oft mit Lock (Mutex) gleichgesetzt, doch kann ein Monitor mit Warten/Benachrichtigen mehr als ein klassischer Lock. In der Definition der Sprache Java (JLS Kapitel 17) tauchen die Begriffe Mutex und Lock allerdings nicht auf; die Autoren sprechen nur von den Monitoraktionen lock und unlock. Die Java Virtual Machine definiert dafür die Opcodes monitorenter und monitorexit.

Java-Konstrukte zum Schutz der kritischen Abschnitte

Wenn wir auf unser Punkteproblem zurückkommen, so stellen wir fest, dass zwei Zeilen auf eine Variable zugreifen:

p.x = x; p.y = y;

int xc = p.x, yc = p.y;

Diese beiden Zeilen bilden also einen kritischen Abschnitt, den jeweils nur ein Thread betreten darf. Wenn also einer der Threads mit p.x = x beginnt, muss er so lange den exklusiven Zugriff bekommen, bis er mit yc = p.y endet.

Aber wie wird nun ein kritischer Abschnitt bekannt gegeben? Zum Markieren und Abschließen dieser Bereiche gibt es zwei Konzepte:

Konstrukt

Eingebautes Schlüsselwort

Java-Standardbibliothek

Schlüsselwort/Typen

synchronized

java.util.concurrent.

locks.Lock

Nutzungsschema

synchronized {

  tue 1

  tue 2

}

lock.lock(); {

  tue 1

  tue 2

}

lock.unlock();*

* Vereinfachte Darstellung, in Abschnitt 15.5.5 mehr

Tabelle 15.6Lock-Konzepte

Beim synchronized entsteht Bytecode, der der JVM sagt, dass ein kritischer Block beginnt und endet. So überwacht die JVM, ob ein zweiter Thread warten muss, wenn er in einen synchronisierten Block eintritt, der schon von einem Thread ausgeführt wird. Bei Lock ist das Ein- und Austreten explizit vom Entwickler programmiert, und vergisst er das, ist das ein Problem. Und während bei der Lock-Implementierung das Objekt, an dem synchronisiert wird, offen hervortritt, ist das bei synchronized nicht so offensichtlich. Hier gilt es, zu wissen, dass jedes Objekt in Java implizit mit einem Monitor verbunden ist. Auf diesen Unterschied kommen wir in Abschnitt 15.5.9, »Lock-Freigabe im Fall von Exceptions«, noch einmal zurück. Da moderne Programme aber mittlerweile mit Lock-Objekten arbeiten, tritt die synchronized-Möglichkeit, die schon Java 1.0 zur Synchronisation bot, etwas in den Hintergrund.

Fassen wir zusammen: Nicht threadsichere Abschnitte müssen geschützt werden. Sie können entweder mit synchronized geschützt werden, bei dem der Eintritt und Austritt implizit geregelt ist, oder durch Lock-Objekte. Befindet sich dann ein Thread in einem geschützten Block und möchte ein zweiter Thread in den Abschnitt, muss er so lange warten, bis der erste Thread den Block wieder freigibt. So ist die Abarbeitung über mehrere Threads einfach synchronisiert, und das Konzept eines Monitors gewährleistet seriellen Zugriff auf kritische Ressourcen. Die kritischen Bereiche sind nicht per se mit einem Monitor verbunden, sondern werden eingerahmt, und dieser Rahmen ist mit einem Monitor (Lock) verbunden.

Mit dem Abschließen und Aufschließen werden wir uns noch intensiver in den folgenden Abschnitten beschäftigen.

 

Zum Seitenanfang

15.5.5Kritische Abschnitte mit ReentrantLock schützen Zur vorigen ÜberschriftZur nächsten Überschrift

Lock ist eine Schnittstelle mit Methoden, mit denen sich ein kritischer Block markieren lässt. Ein Abschnitt beginnt mit lock() und endet mit unlock(). Eine Implementierung der Schnittstelle ist ReentrantLock.

Listing 15.17com/tutego/insel/thread/concurrent/ParallelPointInitSync.java, main()

final Lock lock = new ReentrantLock();

final Point p = new Point();



Runnable r = new Runnable() {

@Override public void run() {

int x = (int)(Math.random() * 1000), y = x;



while ( true ) {

lock.lock();



p.x = x; p.y = y; // *

int xc = p.x, yc = p.y; // *



lock.unlock();



if ( xc != yc )

System.out.println( "Aha: x=" + xc + ", y=" + yc );

}

}

};



new Thread( r ).start();

new Thread( r ).start();

Mit dieser Implementierung wird keine Ausgabe auf dem Bildschirm folgen.

Die Schnittstelle java.util.concurrent.locks.Lock

Lock ist eine Schnittstelle, von der ReentrantLock die wichtigste Implementierung ist. Mit ihr lässt sich der Block betreten und verlassen.

interface java.util.concurrent.locks.Lock
  • void lock()

    Wartet so lange, bis der ausführende Thread den kritischen Abschnitt betreten kann, und markiert ihn dann als betreten. Hat schon ein anderer Thread an diesem Lock-Objekt ein lock() aufgerufen, so muss der aktuelle Thread warten, bis der Lock wieder frei ist. Hat der aktuelle Thread schon den Lock, kann er bei der Implementierung ReentrantLock wiederum lock() aufrufen und sperrt sich nicht selbst.

  • boolean tryLock()

    Wenn der kritische Abschnitt sofort betreten werden kann, ist die Funktionalität wie bei lock(), und die Rückgabe ist true. Ist der Lock gesetzt, so wartet die Methode nicht wie lock(), sondern kehrt mit einem false zurück.

  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException

    Versucht in der angegebenen Zeitspanne den Lock zu bekommen. Das Warten kann mit interrupt() auf dem Thread unterbrochen werden, was tryLock() mit einer Exception beendet.

  • void unlock()

    Verlässt den kritischen Block.

  • void lockInterruptibly() throws InterruptedException

    Wartet wie lock(), um den kritischen Abschnitt betreten zu dürfen, kann aber mit einem interrupt() von außen abgebrochen werden (der lock()-Methode ist ein Interrupt egal). Implementierende Klassen müssen diese Vorgabe nicht zwingend umsetzen, sondern können die Methode auch mit einem einfachen lock() realisieren. ReentrantLock implementiert lockInterruptibly() erwartungsgemäß.

[zB]Beispiel

Wenn wir sofort in den kritischen Abschnitt gehen können, tun wir das; sonst tun wir etwas anderes:

Lock lock = …;

if ( lock.tryLock() ) {

try {



}

finally { lock.unlock(); }

}

else

Klasse ReentrantLock

Die Implementierung ReentrantLock kann noch ein bisschen mehr als lock() und unlock():

class java.util.concurrent.locks.ReentrantLock

implements Lock, Serializable
  • ReentrantLock()

    Erzeugt ein neues Lock-Objekt, das nicht dem am längsten Wartenden den ersten Zugriff gibt.

  • ReentrantLock(boolean fair)

    Erzeugt ein neues Lock-Objekt mit fairem Zugriff, gibt also dem am längsten Wartenden den ersten Zugriff.

  • boolean isLocked()

    Fragt an, ob der Lock gerade genutzt wird und im Moment kein Betreten möglich ist.

  • final int getQueueLength()

    Ermittelt, wie viele auf das Betreten des Blocks warten.

  • int getHoldCount()

    Gibt die Anzahl der erfolgreichen lock()-Aufrufe ohne passendes unlock() zurück. Sollte nach Beenden des Vorgangs 0 sein.

[zB]Beispiel

Das Warten auf den Lock kann unterbrochen werden:

Lock l = new ReentrantLock();

try {

l.lockInterruptibly();

try {



}

finally { l.unlock(); }

}

catch ( InterruptedException e ) { … }

Wenn wir den Lock nicht bekommen haben, dürfen wir ihn auch nicht freigeben!

Die Klasse ReentrantLock implementiert die Schnittstelle Lock.

Abbildung 15.7Die Klasse ReentrantLock implementiert die Schnittstelle Lock.

Schnittstelle ReentrantReadWriteLock

Die Klasse ReentrantLock blockt bei jedem lock() und lässt keinen Interessenten in den kritischen Abschnitt. Viele Szenarien sind jedoch nicht so streng, und so kommt es zu Situationen, in denen lesender Zugriff durchaus von mehreren Parteien möglich ist, schreibender Zugriff aber blockiert wird.

Für diese Lock-Situation gibt es die Schnittstelle ReadWriteLock, die nicht von Lock abgeleitet ist, sondern mit readLock() und writeLock() die Lock-Objekte liefert. Die bisher einzige Implementierung der Schnittstelle ist java.util.concurrent.locks.ReentrantReadWriteLock. Ein Programmausschnitt könnte so aussehen:

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

try {

lock.readLock().lock();



}

finally {

lock.readLock().unlock();

}
Die Klasse ReentrantReadWriteLock implementiert ReadWriteLock.

Abbildung 15.8Die Klasse ReentrantReadWriteLock implementiert ReadWriteLock.

 

Zum Seitenanfang

15.5.6Synchronisieren mit synchronized Zur vorigen ÜberschriftZur nächsten Überschrift

Schon seit Java 1.0 können kritische Abschnitte mit synchronized geschützt werden. Im einfachsten Fall markiert der Modifizierer synchronized die gesamte Methode. Ein betretender Thread setzt bei Objektmethoden den Monitor des this-Objekts und bei statischen Methoden den Lock des dazugehörigen Class-Objekts.

Betritt ein Thread A eine synchronisierte Methode eines Objekts O und versucht anschließend Thread B, eine synchronisierte Methode des gleichen Objekts O aufzurufen, muss der nachfolgende Thread B so lange warten, bis A wieder aus dem synchronisierten Teil austritt. Das geschieht, wenn der erste Thread A die Methode verlässt, denn mit dem Verlassen einer Methode – oder auch einer Ausnahme – gibt die JVM automatisch den Lock frei. Die Dauer eines Locks hängt folglich mit der Dauer des Methodenaufrufs zusammen, was zur Konsequenz hat, dass längere kritische Abschnitte eine mögliche parallele Abarbeitung einschränken und zu längeren Wartezeiten führen. Eine Endlosschleife in der synchronisierten Methode gäbe den Lock niemals frei.

Das aus IPlusPlus.java bekannte Problem mit dem i++ lässt sich mit synchronized einfach lösen:

synchronized void foo() { i++; }

Bei einem Konflikt (mehrere Threads rufen foo() auf) verhindert synchronized, dass sich mehr als ein Thread gleichzeitig im kritischen Abschnitt, dem Rumpf der Methode foo(), befindet. Dies bezieht sich nur auf mehrere Aufrufe von foo() für dasselbe Objekt. Zwei verschiedene Threads können durchaus nebenläufig die Methode foo() für unterschiedliche Objekte ausführen.

[»]Hinweis

Wird eine synchronized-Methode überschrieben, setzt aber die Unterklasse nicht selbst den Modifizierer bei der Methodendeklaration, so wird die Ausführung der Unterklassenmethode nicht synchronisiert. Daher sollte eine überschriebene synchronized-Methode auch immer ein synchronized bekommen, es sei denn natürlich, sie sollte ausdrücklich nichtsynchronisiert sein.

Neben diesem speziellen Problem für atomares Verändern von Variablen lassen sich auch Klassen aus dem Paket java.util.concurrent.atomic verwenden.

inline Bei einem orthografisch anspruchsvollen Wort wie synchronized ist es praktisch, dass Eclipse auch Schlüsselwörter vervollständigt. Hier reicht ein Tippen von sync und (Strg) + Leertaste für einen Dialog.

 

Zum Seitenanfang

15.5.7Mit synchronized synchronisierte Blöcke Zur vorigen ÜberschriftZur nächsten Überschrift

Wenn wir mit Lock-Objekten arbeiten, können wir den Block so fein wählen, wie es erforderlich ist. Mit synchronized haben wir bisher nur eine gesamte Methode sperren können, was in manchen Fällen etwas viel ist. Dann kann eine allgemeinere Variante in Java eingesetzt werden, die nur einen Block synchronisiert. Dazu schreiben wir in Java Folgendes:

synchronized ( objektMitDemMonitor ) {



}

Der Block wird in die geforderten geschweiften Klammern gesetzt, und hinter dem Schlüsselwort in Klammern muss ein Objekt stehen, das den zu verwendenden Monitor besitzt. Die Konsequenz ist die Möglichkeit, über einen beliebigen Monitor zu synchronisieren und nicht unbedingt über den Monitor des Objekts, für das die synchronisierte Methode aufgerufen wurde, wie es bei synchronisierten Objektmethoden üblich ist.

[»]Hinweis

Eine synchronisierte Objektmethode ist nichts anderes als eine Variante von:

synchronized( this )

{

// Code der Methode.

}

Statisch synchronisierte Blöcke

Nicht nur Objektmethoden, sondern auch Klassenmethoden können synchronized sein. Doch die Nachbildung in einem Block sieht etwas anders aus, da es keine this-Referenz gibt. Hier kann ein Object-Exemplar für einen Lock herhalten, der extra für die Klasse angelegt wird. Dies ist eines der seltenen Beispiele, in denen ein Exemplar der Klasse Object Sinn ergibt:

Listing 15.18com/tutego/insel/thread/StaticSync.java

package com.tutego.insel.thread;



class StaticSync {

private static final Object o = new Object();

static void staticFoo() {

synchronized( o ) {

// ...

}

}

}

Alternativ könnten wir auch das zugehörige Class-Objekt einsetzen. Wir müssen das entsprechende Klassenobjekt dann nur mittels StaticSync.class erfragen. Würden wir gleich mit Lock-Objekten arbeiten, stellte sich die Frage erst gar nicht.

[»]Hinweis

Bei Lock-Objekten oder synchronized-Blöcken kann der zwingend synchronisierbare Teil in einem kleinen Abschnitt bleiben. Die JVM kann die anderen Teile nebenläufig abarbeiten, und andere Threads dürfen die anderen Teile betreten. Als Resultat ergibt sich eine verbesserte Geschwindigkeit.

 

Zum Seitenanfang

15.5.8Dann machen wir doch gleich alles synchronisiert! Zur vorigen ÜberschriftZur nächsten Überschrift

In nebenläufigen Programmen kann es schnell zu unerwünschten Nebeneffekten kommen. Das ist auch der Grund, warum threadlastige Programme schwer zu debuggen sind. Warum sollten wir also nicht alle Methoden synchronisieren? Wäre dann nicht das Problem aus der Welt geschafft? Prinzipiell würde das einige Probleme lösen, doch hätten wir uns damit andere Nachteile eingefangen:

  • Methoden, die synchronisiert sind, müssen von der JVM besonders bedacht werden, damit keine zwei Threads die Methode für das gleiche Objekt ausführen. Wenn also ein zweiter Thread in die Methode eintreten möchte, kann er das nicht einfach machen, sondern muss vielleicht erst neben vielen anderen Threads warten. Es muss also eine Datenstruktur geben, in der wartende Threads eingetragen und ausgewählt werden. Das kostet zusätzlich Zeit und ist im Vergleich zu einem normalen Methodenaufruf teurer.

  • Zusätzlich kommt ein Problem hinzu, wenn eine nicht notwendigerweise, also überflüssigerweise, synchronisierte Methode eine Endlosschleife oder lange Operationen durchführt. Dann warten alle anderen Threads auf die Freigabe, und das kann im Fall der Endlosschleife ewig sein. Auch bei Multiprozessorsystemen profitieren wir nicht von dieser Programmiertechnik. Unnötig und falsch synchronisierte Blöcke machen die Vorteile von Mehrprozessormaschinen zunichte.

  • Wenn alle Methoden synchronisiert sind, steigt auch die Gefahr eines Deadlocks. In den folgenden Abschnitten erfahren wir etwas mehr über den Fall, dass zwei Threads wechselseitig auf Ressourcen eines jeweils anderen Threads zugreifen wollen und sich dabei im Weg stehen.

Ist der gesamte Zugriff auf ein Objekt synchronisiert und kann kein zweiter Thread irgendwelche Eigenschaften nebenläufig zu einem anderen Thread nutzen, nennt sich das Objekt voll synchronisiert im Gegensatz zu teilsynchronisiert. Sind einige Methoden der Klasse nicht synchronisiert, kann ein zweiter Thread nebenläufig zu den synchronisierten Blöcken an die Eigenschaften gehen.

 

Zum Seitenanfang

15.5.9Lock-Freigabe im Fall von Exceptions Zur vorigen ÜberschriftZur nächsten Überschrift

Kommt es innerhalb eines synchronized-Blocks oder innerhalb einer synchronisierten Methode zu einer nicht überprüften RuntimeException, wird die JVM den Lock automatisch freigeben. Der Grund: Die Laufzeitumgebung gibt den Lock automatisch frei, wenn der Thread den synchronisierten Block verlässt, was bei einer Exception der Fall ist.

Werden die mit dem Schlüsselwort synchronized geschützten Blöcke durch Lock-Objekte umgesetzt, ist darauf zu achten, die Locks auch im Exception-Fall wieder freizugeben. Ein finally mit unlock() kommt da gerade recht, denn finally wird ja immer ausgeführt, egal, ob es einen Fehler gab oder nicht:

Listing 15.19com/tutego/insel/thread/concurrent/UnlockInFinally.java, main()

ReentrantLock lock = new ReentrantLock();



try {

lock.lock();



try {

System.out.println( lock.getHoldCount() ); // 1

System.out.println( 12 / 0 );

}

finally {

lock.unlock();

}

}

catch ( Exception e ) {

System.out.println( e.getMessage() ); // / by zero

}

System.out.println( lock.getHoldCount() ); // 0

Nach dem lock() liefert getHoldCount() 1, da ein Thread den Block betreten hat. Die Division durch null provoziert eine RuntimeException, und finally gibt den Lock frei. Die Ausnahme wird abgefangen, und getHoldCount() liefert wieder 0, da finally das unlock() ausgeführt hat. Würden wir die Zeile mit unlock() auskommentieren, so würde getHoldCount() weiterhin 1 liefern, was ein Fehler ist.

 

Zum Seitenanfang

15.5.10Deadlocks Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Deadlock (zu Deutsch etwa Blockade) kommt beispielsweise dann vor, wenn ein Thread A eine Ressource belegt, die ein anderer Thread B haben möchte, und Thread B eine Ressource belegt, die A gerne nutzen würde. In dieser Situation können beide nicht vor und zurück und befinden sich in einem dauernden Wartezustand. Deadlocks können in Java-Programmen nicht erkannt und verhindert werden. Uns fällt also die Aufgabe zu, diesen ungünstigen Zustand gar nicht erst herbeizuführen.

Das nächste Beispiel soll über eine Verklemmung einen Deadlock provozieren. Zwei Threads wetteifern um die Lock-Objekte lock1 und lock2. Dabei kommt es zu einem Deadlock, da der eine genau den einen Lock besetzt, den der jeweils andere zum Weiterarbeiten benötigt:

Listing 15.20com/tutego/insel/thread/Deadlock.java

package com.tutego.insel.thread;



import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.*;



class Deadlock {

static Lock lock1 = new ReentrantLock(),

lock2 = new ReentrantLock();



static class T1 extends Thread {

@Override public void run() {

lock1.lock();

System.out.println( "T1: Lock auf lock1 bekommen" );



try { TimeUnit.SECONDS.sleep( 1 ); } catch ( InterruptedException e ) { }



lock2.lock();

System.out.println( "T1: Lock auf lock2 bekommen" );



lock2.unlock();

lock1.unlock();

}

}



static class T2 extends Thread {

@Override public void run()

{

lock2.lock();

System.out.println( "T2: Lock auf lock2 bekommen" );



lock1.lock();

System.out.println( "T2: Lock auf lock1 bekommen" );



lock1.unlock();

lock2.unlock();

}

}



public static void main( String[] args ) {

new T1().start();

new T2().start();

}

}

In der Ausgabe sehen wir nur zwei Zeilen, und schon hängt das gesamte Programm:

T1: Lock auf lock1 bekommen

T2: Lock auf lock2 bekommen

Eine Lösung des Problems wäre, bei geschachteltem Synchronisieren auf mehrere Objekte diese immer in der gleichen Reihenfolge zu belegen, also etwa immer erst lock1, dann lock2. Bei unbekannten, dynamisch wechselnden Objekten muss dann unter Umständen eine willkürliche Ordnung festgelegt werden.

[»]Hinweis

Die JVM von Oracle verfügt über eine eingebaute Deadlock-Erkennung, die auf der Konsole aktiviert werden kann. Dazu ist unter Windows die Tastenkombination (Strg) + (Pause) zu drücken und unter Linux oder Solaris (Strg) + (\).

 


Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück
 Zum Katalog
Zum Katalog: Java ist auch eine Insel Java ist auch eine Insel

Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?

Ihre Meinung



 Buchempfehlungen
Zum Katalog: Java ist auch eine Insel

Java ist auch eine Insel




Zum Katalog: Java SE 9-Standard-Bibliothek

Java SE 9-Standard-Bibliothek




Zum Katalog: Professionell entwickeln mit Java EE 8

Professionell entwickeln mit Java EE 8




Zum Katalog: Entwurfsmuster

Entwurfsmuster




Zum Katalog: IT-Projektmanagement

IT-Projektmanagement




 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2017

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de