Galileo Computing < openbook >Galileo 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 Exceptions
7 Äußere.innere Klassen
8 Besondere Klassen der Java SE
9 Generics<T>
10 Architektur, Design und angewandte Objektorientierung
11 Die Klassenbibliothek
12 Einführung in die nebenläufige Programmierung
13 Einführung in Datenstrukturen und Algorithmen
14 Einführung in grafische Oberflächen
15 Einführung in Dateien und Datenströme
16 Einführung in die <XML>-Verarbeitung mit Java
17 Einführung ins Datenbankmanagement mit JDBC
18 Bits und Bytes und Mathematisches
19 Die Werkzeuge des JDK
A Die Klassenbibliothek
Stichwort

Download:
- openbook, ca. 24,5 MB
- Aufgaben, ca. 1,1 MB
- Programme, ca. 12,8 MB
Buch bestellen
Ihre Meinung?

Spacer
Java ist auch eine Insel von Christian Ullenboom
Das umfassende Handbuch
Buch: Java ist auch eine Insel

Java ist auch eine Insel
Galileo Computing
1308 S., 10., aktualisierte Auflage, geb., mit DVD
ca. 49,90 Euro, ISBN 978-3-8362-1802-3
Pfeil12 Einführung in die nebenläufige Programmierung
Pfeil12.1 Nebenläufigkeit
Pfeil12.1.1 Threads und Prozesse
Pfeil12.1.2 Wie parallele Programme die Geschwindigkeit steigern können
Pfeil12.1.3 Was Java für Nebenläufigkeit alles bietet
Pfeil12.2 Threads erzeugen
Pfeil12.2.1 Threads über die Schnittstelle Runnable implementieren
Pfeil12.2.2 Thread mit Runnable starten
Pfeil12.2.3 Die Klasse Thread erweitern
Pfeil12.3 Thread-Eigenschaften und -Zustände
Pfeil12.3.1 Der Name eines Threads
Pfeil12.3.2 Wer bin ich?
Pfeil12.3.3 Schläfer gesucht
Pfeil12.3.4 Mit yield() auf Rechenzeit verzichten
Pfeil12.3.5 Der Thread als Dämon
Pfeil12.3.6 Das Ende eines Threads
Pfeil12.3.7 Einen Thread höflich mit Interrupt beenden
Pfeil12.3.8 UncaughtExceptionHandler für unbehandelte Ausnahmen
Pfeil12.4 Der Ausführer (Executor) kommt
Pfeil12.4.1 Die Schnittstelle Executor
Pfeil12.4.2 Die Thread-Pools
Pfeil12.5 Synchronisation über kritische Abschnitte
Pfeil12.5.1 Gemeinsam genutzte Daten
Pfeil12.5.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
Pfeil12.5.3 Punkte parallel initialisieren
Pfeil12.5.4 Kritische Abschnitte schützen
Pfeil12.5.5 Kritische Abschnitte mit ReentrantLock schützen
Pfeil12.6 Zum Weiterlesen

Galileo Computing - Zum Seitenanfang

12.5 Synchronisation über kritische AbschnitteZur 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.


Galileo Computing - Zum Seitenanfang

12.5.1 Gemeinsam genutzte DatenZur nächsten ÜberschriftZur vorigen Ü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.


Galileo Computing - Zum Seitenanfang

12.5.2 Probleme beim gemeinsamen Zugriff und kritische AbschnitteZur nächsten ÜberschriftZur vorigen Ü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, parallel 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 thread-sicher (engl. thread-safe).

Wir werden uns nun Beispiele für kritische Abschnitte anschauen und dann sehen, wie wir diese 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 thread-sicher, 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 thread-eigenen 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.

Thread-sichere und nicht thread-sichere Klassen der Java Bibliothek

Es gibt in Java viele Klassen, die nicht thread-sicher 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 thread-sicher sind.

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

Tabelle 12.2: Thread-sichere und nicht thread-sichere Klassen

Nicht thread-sicher Thread-sicher
StringBuilder StringBuffer
ArrayList Vector
HashMap Hashtable

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


Galileo Computing - Zum Seitenanfang

12.5.3 Punkte parallel initialisierenZur nächsten ÜberschriftZur vorigen Ü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.

Tabelle 12.3: Zwei Threads belegen beide den Punkt p

Thread T1 Thread T2

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

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

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:

Tabelle 12.4: Mögliche sequentielle Abarbeitung der Punktbelegung

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

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 nicht-atomare 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 12.11: com/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 parallelen Punkten 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 beziehungsweise race hazard (zu Deutsch auch Wettlaufsituation).


Galileo Computing - Zum Seitenanfang

12.5.4 Kritische Abschnitte schützenZur nächsten ÜberschriftZur vorigen Ü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[186](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, das 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 Monitor-Aktionen 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 Punkte-Problem 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:

Tabelle 12.5: Lock-Konzepte

Konstrukt Eingebautes Schlüsselwort Java-Standardbibliothek
Schlüsselwort/Typen synchronized java.util.concurrent.locks.Lock
Nutzungsschema

synchronized
{
Tue1
Tue2
}

lock.lock();
{
Tue1
Tue2
}
lock.unlock();[187](Vereinfachte Darstellung, später mehr.)

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. 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 thread-sichere 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.


Galileo Computing - Zum Seitenanfang

12.5.5 Kritische Abschnitte mit ReentrantLock schützenZur nächsten ÜberschriftZur vorigen Überschrift

Seit Java 5 gibt es die Schnittstelle Lock, mit der sich ein kritischer Block markieren lässt. Ein Abschnitt beginnt mit lock() und endet mit unlock():

Listing 12.12: com/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äß.
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
...

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.
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!

Abbildung

Abbildung 12.7: Die Klasse ReentrantLock implementiert die Schnittstelle Lock



Ihr Kommentar

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







<< 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 7 – Mehr als eine Insel





 Java 7 –
 Mehr als eine Insel


Zum Katalog: Android 3






 Android 3


Zum Katalog: Android-Apps entwickeln






 Android-Apps
 entwickeln


Zum Katalog: NetBeans Platform 7






 NetBeans
 Platform 7


Zum Katalog: Einstieg in Eclipse 3.7






 Einstieg in
 Eclipse 3.7


Zum Katalog: Einstieg in Java






 Einstieg
 in Java


Zum Katalog: Einstieg in Java 7






 Einstieg in
 Java 7


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Galileo Press 2011
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.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de