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

 << zurück
Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5
Java ist auch eine Insel

Java ist auch eine Insel
5., akt. und erw. Auflage
1454 S., mit CD, 49,90 Euro
Galileo Computing
ISBN 3-89842-747-1
gp Kapitel 9 Threads und nebenläufige Programmierung
  gp 9.1 Prozesse und Threads
    gp 9.1.1 Wie parallele Programme die Geschwindigkeit steigern können
  gp 9.2 Threads erzeugen
    gp 9.2.1 Threads über die Schnittstelle Runnable implementieren
    gp 9.2.2 Thread mit Runnable starten
    gp 9.2.3 Der Name eines Threads
    gp 9.2.4 Die Klasse Thread erweitern
    gp 9.2.5 Wer bin ich?
  gp 9.3 Der Ausführer (Executor) kommt
    gp 9.3.1 Die Schnittstelle Executor
    gp 9.3.2 Die Thread-Pools
    gp 9.3.3 Threads mit Rückgabe über Callable
    gp 9.3.4 Mehrere Callable abarbeiten
    gp 9.3.5 Mit ScheduledExecutorService wiederholende Ausgaben und Zeitsteuerungen
  gp 9.4 Die Zustände eines Threads
    gp 9.4.1 Threads schlafen
    gp 9.4.2 Das Ende eines Threads
    gp 9.4.3 UncaughtExceptionHandler für unbehandelte Ausnahmen
    gp 9.4.4 Einen Thread höflich mit Interrupt beenden
    gp 9.4.5 Der stop() von außen und die Rettung mit ThreadDeath
    gp 9.4.6 Ein Rendezvous mit join() und Barrier sowie Austausch mit Exchanger
    gp 9.4.7 Mit yield() auf Rechenzeit verzichten
    gp 9.4.8 Arbeit niederlegen und wieder aufnehmen
    gp 9.4.9 Priorität
    gp 9.4.10 Der Thread ist ein Dämon
  gp 9.5 Synchronisation über kritische Abschnitte
    gp 9.5.1 Gemeinsam genutzte Daten
    gp 9.5.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
    gp 9.5.3 Punkte parallel initialisieren
    gp 9.5.4 i++ sieht atomar aus, ist es aber nicht
    gp 9.5.5 Kritische Abschnitte schützen
    gp 9.5.6 Schützen mit ReentrantLock
    gp 9.5.7 Synchronisieren mit synchronized
    gp 9.5.8 Synchronized-Methoden der Klasse StringBuffer
    gp 9.5.9 Mit synchronized synchronisierte Blöcke
    gp 9.5.10 Look-Freigabe im Fall von Exceptions
    gp 9.5.11 Mit synchronized nachträglich synchronisieren
    gp 9.5.12 Monitore sind reentrant, gut für die Geschwindigkeit
    gp 9.5.13 Synchronisierte Methodenaufrufe zusammenfassen
    gp 9.5.14 Deadlocks
    gp 9.5.15 Erkennen von Deadlocks
  gp 9.6 Synchronisation über Warten und Benachrichtigen
    gp 9.6.1 Die Schnittstelle Condition
    gp 9.6.2 Beispiel Erzeuger-Verbraucher-Programm
    gp 9.6.3 Warten mit wait() und Aufwecken mit notify()
    gp 9.6.4 Falls der Lock fehlt: IllegalMonitorStateException
    gp 9.6.5 Semaphoren
  gp 9.7 Atomares und frische Werte mit volatile
    gp 9.7.1 Der Modifizierer volatile bei Objekt-/Klassenvariablen
    gp 9.7.2 Das Paket java.util.concurrent.atomic
  gp 9.8 Mit dem Thread verbundene Variablen
    gp 9.8.1 ThreadLocal
    gp 9.8.2 InheritableThreadLocal
  gp 9.9 Gruppen von Threads in einer Thread-Gruppe
    gp 9.9.1 Aktive Threads in der Umgebung
    gp 9.9.2 Etwas über die aktuelle Thread-Gruppe herausfinden
    gp 9.9.3 Threads in einer Thread-Gruppe anlegen
    gp 9.9.4 Methoden von Thread und ThreadGroup im Vergleich
  gp 9.10 Die Klassen Timer und TimerTask
    gp 9.10.1 Job-Scheduler Quartz
  gp 9.11 Einen Abbruch der virtuellen Maschine erkennen


Galileo Computing

9.7 Atomares und frische Werte mit volatildowntop

Die JVM arbeitet bei den Ganzzahl-Datentypen kleiner gleich int intern nur mit einem int. Doch obwohl es auch für die 64-Bit-Datentypen long und double einzelne Bytecode-Befehle gibt, ist nicht gesichert, dass die Operationen auf diesen Datentypen unteilbar sind, da etwa eine 64-Bit-Zuweisung sich aus zwei 32-Bit Zuweisungen zusammensetzen lässt. Es kann also passieren, dass ein Thread mitten in einer long- oder double-Operation von einem anderen Thread verdrängt wird. Greifen zwei Threads auf die gleiche 64-Bit Variable zu, so könnte möglicherweise der eine Thread eine Hälfte schreiben und der andere Thread die andere.


Beispiel   Zwei Threads versuchen gleichzeitig die Variable l vom Typ long zu ändern. (Aus Gründen der Übersichtlichkeit ist ein Leerzeichen nach dem ersten 32-Bit-Block in der hexadezimalen Notation eingefügt.)
l = 0x00000000 00000000             l = 0xFFFFFFFF FFFFFFFF
Schreibt der erste Thread den ersten Teil der long-Variable mit 00000000 und findet dann ein Kontextwechsel statt, sodass der zweite Thread die Variable komplett mit FFFFFFFF FFFFFFFF initialisiert, gibt es nach dem erneuten Umschalten ein Problem, weil dann der erste Thread seine zweite Hälfe schreiben wird und die Belegung der Variablen FFFFFF 00000000 ist. Insgesamt gibt es vier denkbare Belegungen durch das Scheduling beim Kontext-Wechsel:
l = 0x00000000 00000000
l = 0xFFFFFFFF FFFFFFFF
l = 0x00000000 FFFFFFFF
l = 0xFFFFFFFF 00000000


Galileo Computing

9.7.1 Der Modifizierer volatile bei Objekt-/Klassenvariabledowntop

Um dies zu vermeiden, können die Objekt- und Klassenvariablen mit dem Modifizierer volatile deklariert werden. Die Zugriffsoperationen werden auf diesen Variablen dann atomar ausgeführt. Der Modifizierer ist bei lokalen Variablen nicht gestattet, da sie auf dem Stapel liegen und für andere Threads nicht zugänglich sind. Achtung: Auch mit volatile sind Operationen wie i++ sind damit natürlich noch nicht atomar, da i++ aus Grundoperationen besteht: Lesen von i, erhören um Eins und schreiben von i; unser Kapitel über synchronisierte Blöcke nannte die Teile schon.

Zwischenspeicherung untersagen

volatile beugt zusätzlich einem anderen Problem vor. Während der Berechnung könnte die Laufzeitumgebung Inhalte von Variablen im Prozessorspeicher (zum Beispiel Register) zwischengespeichert haben. Das passiert etwa, wenn in einer Schleife ein Wert immer hoch gezählt wird, der in einer Objektvariablen gespeichert wird.

Eine Laufzeitumgebung könnte den Zugriff auf Objektvariablen optimieren, indem zuerst der Variableninhalt in eine interne lokale Variable kopiert und anschließend, nach einer Berechnung, das Ergebnis zurückgespeichert wird. Ist cnt eine Objektvariable, dann könnte folgende Schleife von der Laufzeitumgebung optimiert werden:

for ( int i = 0; i < 1000000; i++ )
  cnt++;

Eine optimierende Laufzeitumgebung könnte nun auf die Idee kommen, nicht bei jedem Schleifendurchlauf die Objektvariable zu erhöhen, sondern nur am Ende. Wenn die Umgebung i zuerst in eine interne lokale Variable kopiert, dann die gesamte Schleife ausführt und erst anschließend den internen Wert nach i zurückkopiert, haben wir viel Zeit gespart, denn der Zugriff auf eine lokale Variable, die sich im Register des Prozessors aufhalten kann, ist wesentlich schneller als der Zugriff auf eine Objektvariable. Dies ist im Übrigen eine beliebte Strategie, um die Performanz eines Programms zu steigern. Mit dieser internen Optimierung kommt es jedoch zu schwer kontrollierbaren Nebeneffekten, und Änderungen am Wert von i sind nicht sofort für andere Threads sichtbar, denen damit die Information über die einzelnen Inkrementschritte fehlt. Oder ein anderes Beispiel:

int i = cnt;
Thread.sleep( 10000 );
int j = cnt;

Verändert ein anderer Thread während des Wartens die Variable cnt, könnte dennoch i = j sein, wenn die Laufzeitumgebung die Variable j nicht frisch mit dem Wert aus cnt initialisiert.

Auch hier hilft wieder das ungewöhnliche Schlüsselwort. Ist die Variable mit volatile gekennzeichnet, wird das Ergebnis nicht im Zwischenspeicher belassen, sondern ständig aktualisiert. Die parallelen Threads sehen somit immer den korrekten Variablenwert, da er vor jeder Benutzung aus dem Speicher gelesen und nach einer Änderung sofort wieder zurückgeschrieben wird. Sehr gut ist das für boolean-Werte, die Flags anzeigen, die andere Thread zur Synchronisation beobachten. Ein Array, das mit volatile markiert ist, hat alle Zugriffe auf die Elemente volatile, denn auch hier könnte die JVM die Elemente aus Performanzgründen zwischenspeichern. Wohlgemerkt bleiben nur die Werte aktuell, nicht-atomare Operationen wie cnt++ sind damit noch nicht atomar!


Galileo Computing

9.7.2 Das Paket java.util.concurrent.atomic  toptop

Während volatile Lesen und Schreiben auf den 64-Bit-Datentypen atomar macht, ist eine Operation wie cnt++ oder cnt > 12 nicht atomar. Kurz vor dem Vergleich kann ein Thread-Wechsel stattfinden und cnt geändert werden, sodass der Vergleich einen anderen Ausgang findet. Die Rettung ist ein synchronisierter Block, wobei jedoch eine Synchronisationsvariable nötig ist.

Seit Java 5 helfen hier die Klassen des Pakets java.util.concurrent.atomic. Ein Beispiel für einen veränderbaren Behälter ist AtomicInteger, der Operationen wie getAndIncrement() anbietet, die den Wert des Behälters atomar erhöhen.


Beispiel   Eine öffentliche Klasse gibt über eine statische Methode eine ID (im Breich long).
import java.util.concurrent.atomic.AtomicLong;
public class Id
{
  private static AtomicLong id = new AtomicLong();
  private Id() { /* Empty */ }
  public long next()
  {
    return id.getAndIncrement();
  }
}

Insgesamt befinden sich im Paket java.util.concurrent.atomic folgende Klassen für elementare Datentypen: AtomicBoolean, AtomicInteger, AtomicLong – es gibt weder AtomicShort noch AtomicByte. Da diese Objekte alle veränderbar sind, eignen sie sich nicht unbedingt als Ersatz für immutable Wrapper-Objekte wie Integer, insbesondere nicht in allgemeinen Assoziativspeichern oder Mengen; sie definierten auch gar kein eigenes equals() und hashCode().

Für Felder stehen bereit: AtomicIntegerArray, AtomicLongArray. Die Klassen AtomicIntegerFieldUpdater<T> und AtomicLongFieldUpdater<T> gehen über Reflection an Attribute heran, die volatile sind, und ermöglichen atomare Operationen. Dazu kommen noch einige Klassen zum atomaren Arbeiten mit Referenzen: AtomicReference, AtomicReferenceArray<E>, AtomicReferenceFieldUpdater<T,V>, AtomicMarkableReference<V> (assoziiert ein Objekt mit einem Flag, welches atomar geändert werden kann) und AtomicStampedReference<V> (assoziiert Objekt mit einer Ganzzahl).

 << zurück




Copyright © Galileo Press GmbH 2005
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 GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de