Java SE 8 Standard-Bibliothek  
Professionelle Bücher. Auch für Einsteiger.
 
Inhaltsverzeichnis


Vorwort
1 Neues in Java 8 und Java 7
2 Fortgeschrittene String-Verarbeitung
3 Threads und nebenläufige Programmierung
4 Datenstrukturen und Algorithmen
5 Raum und Zeit
6 Dateien, Verzeichnisse und Dateizugriffe
7 Datenströme
8 Die eXtensible Markup Language (XML)
9 Dateiformate
10 Grafische Oberflächen mit Swing
11 Grafikprogrammierung
12 JavaFX
13 Netzwerkprogrammierung
14 Verteilte Programmierung mit RMI
15 RESTful und SOAP-Web-Services
16 Technologien für die Infrastruktur
17 Typen, Reflection und Annotationen
18 Dynamische Übersetzung und Skriptsprachen
19 Logging und Monitoring
20 Sicherheitskonzepte
21 Datenbankmanagement mit JDBC
22 Java Native Interface (JNI)
23 Dienstprogramme für die Java-Umgebung
Stichwortverzeichnis

Jetzt Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Java SE 8 Standard-Bibliothek von Christian Ullenboom
Das Handbuch für Java-Entwickler
Buch: Java SE 8 Standard-Bibliothek

Java SE 8 Standard-Bibliothek
Pfeil 3 Threads und nebenläufige Programmierung
Pfeil 3.1 Threads erzeugen
Pfeil 3.1.1 Threads über die Schnittstelle Runnable implementieren
Pfeil 3.1.2 Thread mit Runnable starten
Pfeil 3.1.3 Die Klasse Thread erweitern
Pfeil 3.2 Thread-Eigenschaften und -Zustände
Pfeil 3.2.1 Der Name eines Threads
Pfeil 3.2.2 Wer bin ich?
Pfeil 3.2.3 Die Zustände eines Threads *
Pfeil 3.2.4 Schläfer gesucht
Pfeil 3.2.5 Mit yield() auf Rechenzeit verzichten
Pfeil 3.2.6 Der Thread als Dämon
Pfeil 3.2.7 Freiheit für den Thread – das Ende
Pfeil 3.2.8 Einen Thread höflich mit Interrupt beenden
Pfeil 3.2.9 UncaughtExceptionHandler für unbehandelte Ausnahmen
Pfeil 3.2.10 Der stop() von außen und die Rettung mit ThreadDeath *
Pfeil 3.2.11 Ein Rendezvous mit join(…) *
Pfeil 3.2.12 Arbeit niederlegen und wieder aufnehmen *
Pfeil 3.2.13 Priorität *
Pfeil 3.3 Der Ausführer (Executor) kommt
Pfeil 3.3.1 Die Schnittstelle Executor
Pfeil 3.3.2 Glücklich in der Gruppe – die Thread-Pools
Pfeil 3.3.3 Threads mit Rückgabe über Callable
Pfeil 3.3.4 Mehrere Callable abarbeiten
Pfeil 3.3.5 ScheduledExecutorService für wiederholende Ausgaben und Zeitsteuerungen nutzen
Pfeil 3.4 Synchronisation über kritische Abschnitte
Pfeil 3.4.1 Gemeinsam genutzte Daten
Pfeil 3.4.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
Pfeil 3.4.3 Punkte nebenläufig initialisieren
Pfeil 3.4.4 i++ sieht atomar aus, ist es aber nicht *
Pfeil 3.4.5 Kritische Abschnitte schützen
Pfeil 3.4.6 Kritische Abschnitte mit ReentrantLock schützen
Pfeil 3.4.7 Synchronisieren mit synchronized
Pfeil 3.4.8 Synchronized-Methoden der Klasse StringBuffer *
Pfeil 3.4.9 Mit synchronized synchronisierte Blöcke
Pfeil 3.4.10 Dann machen wir doch gleich alles synchronisiert!
Pfeil 3.4.11 Lock-Freigabe im Fall von Exceptions
Pfeil 3.4.12 Deadlocks
Pfeil 3.4.13 Mit synchronized nachträglich synchronisieren *
Pfeil 3.4.14 Monitore sind reentrant – gut für die Geschwindigkeit *
Pfeil 3.4.15 Synchronisierte Methodenaufrufe zusammenfassen *
Pfeil 3.5 Synchronisation über Warten und Benachrichtigen
Pfeil 3.5.1 Die Schnittstelle Condition
Pfeil 3.5.2 It’s Disco-Time *
Pfeil 3.5.3 Warten mit wait(…) und Aufwecken mit notify()/notifyAll() *
Pfeil 3.5.4 Falls der Lock fehlt – IllegalMonitorStateException *
Pfeil 3.6 Datensynchronisation durch besondere Concurrency-Klassen *
Pfeil 3.6.1 Semaphor
Pfeil 3.6.2 Barrier und Austausch
Pfeil 3.6.3 Stop and go mit Exchanger
Pfeil 3.7 Atomare Operationen und frische Werte mit volatile *
Pfeil 3.7.1 Der Modifizierer volatile bei Objekt-/Klassenvariablen
Pfeil 3.7.2 Das Paket java.util.concurrent.atomic
Pfeil 3.8 Teile und herrsche mit Fork und Join *
Pfeil 3.8.1 Algorithmendesign per »teile und herrsche«
Pfeil 3.8.2 Nebenläufiges Lösen von D&C-Algorithmen
Pfeil 3.8.3 Fork und Join
Pfeil 3.9 CompletionStage und CompletableFuture *
Pfeil 3.10 Mit dem Thread verbundene Variablen *
Pfeil 3.10.1 ThreadLocal
Pfeil 3.10.2 InheritableThreadLocal
Pfeil 3.10.3 ThreadLocalRandom als schneller nebenläufiger Zufallszahlengenerator
Pfeil 3.10.4 ThreadLocal bei der Performance-Optimierung
Pfeil 3.11 Threads in einer Thread-Gruppe *
Pfeil 3.11.1 Aktive Threads in der Umgebung
Pfeil 3.11.2 Etwas über die aktuelle Thread-Gruppe herausfinden
Pfeil 3.11.3 Threads in einer Thread-Gruppe anlegen
Pfeil 3.11.4 Methoden von Thread und ThreadGroup im Vergleich
Pfeil 3.12 Zeitgesteuerte Abläufe
Pfeil 3.12.1 Die Typen Timer und TimerTask
Pfeil 3.12.2 Job-Scheduler Quartz
Pfeil 3.13 Einen Abbruch der virtuellen Maschine erkennen
Pfeil 3.13.1 Shutdown-Hook
Pfeil 3.13.2 Signale
Pfeil 3.14 Zum Weiterlesen
 
Zum Seitenanfang

3.7Atomare Operationen und frische Werte mit volatile * Zur vorigen ÜberschriftZur nächsten Überschrift

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 sich etwa eine 64-Bit-Zuweisung 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.

[zB]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-Variablen 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älfte schreiben wird und die Belegung der Variablen FFFFFFFF 00000000 ist. Insgesamt gibt es vier denkbare Belegungen durch das Scheduling beim Kontextwechsel:

l = 0x00000000 00000000
l = 0xFFFFFFFF FFFFFFFF
l = 0x00000000 FFFFFFFF
l = 0xFFFFFFFF 00000000
 
Zum Seitenanfang

3.7.1Der Modifizierer volatile bei Objekt-/Klassenvariablen Zur vorigen ÜberschriftZur nächsten Überschrift

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++ natürlich noch nicht atomar, da i++ aus Grundoperationen besteht: Lesen von i, Erhöhen um eins und Schreiben von i; Abschnitt 3.4.9, »Mit synchronized 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 hochgezählt wird, der in einer Objektvariablen gespeichert wird.

Eine Laufzeitumgebung könnte den (teureren) Zugriff auf Objektvariablen optimieren, indem sie zuerst den Variableninhalt in eine (schnelle) interne lokale Variable kopiert und anschließend, nach einer Berechnung, das Ergebnis zurückgespeichert. Nehmen wir eine Objektvariable cnt und folgende Schleife an:

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

Eine optimierende Laufzeitumgebung könnte nun auf die Idee kommen, nicht bei jedem Schleifendurchlauf den Speicher der Objektvariablen cnt auszulesen und zu beschreiben, sondern nur am Ende, etwa so:

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

Wenn die Umgebung cnt zuerst in eine interne lokale Variable kopiert, dann die gesamte Schleife ausführt und erst anschließend den internen Wert nach cnt zurückkopiert, hätten 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 Performance eines Programms zu steigern. Mit dieser internen Optimierung kommt es jedoch zu schwer kontrollierbaren Nebeneffekten, und Änderungen am Wert von cnt 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 j = i sein, wenn die Laufzeitumgebung die Variable j nicht frisch mit dem Wert aus cnt initialisiert. Nebenläufige Programmierung und Programmoptimierung passen schwer zusammen!

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 nebenläufigen 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 Threads 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 Performance-Gründen zwischenspeichern. Wohlgemerkt bleiben nur die Werte aktuell; nichtatomare Operationen wie cnt++ sind damit immer noch nicht atomar.

 
Zum Seitenanfang

3.7.2Das Paket java.util.concurrent.atomic Zur vorigen ÜberschriftZur nächsten Überschrift

Während volatile das Lesen und Schreiben auf den 64-Bit-Datentypen atomar macht, ist eine Operation wie cnt++ oder cnt > 12 nicht atomar. Kurz vor dem Erhöhen oder 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.

Noch einfacher geht es mit den Klassen im Paket java.util.concurrent.atomic, die Operationen auf Werten durchführen, ohne dass Entwickler sich selbst um die Synchronisation kümmern müssen.

AtomicXXX-Klassen

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.

[zB]Beispiel

Eine öffentliche Klasse bietet über eine statische Methode eine ID (im Bereich long) an:

Listing 3.38com/tutego/insel/thread/atomic/Id.java

public class Id {

private static final 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 deklarieren 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, das atomar geändert werden kann) und AtomicStampedReference<V> (assoziiert ein Objekt mit einer Ganzzahl).

LongAdder und DoubleAdder

Die AtomicXXX-Klassen sind gut, wenn es nicht zu viele parallele Threads gibt, die gleichzeitig die AtomicXXX-Exemplare verändern. Der Grund ist einfach: Jeder Thread muss warten, bis ein anderer Thread die Veränderung am AtomicXXX vorgenommen hat. Stehen also 100 Threads in der Schlange, um AtomicXXX zu verändern, werden sie erst nacheinander abgearbeitet – das geht zwar an sich schnell, dennoch führt die sequenzielle Verarbeitung zu Wartesituationen.

Wenn es wirklich viele nebenläufige Threads gibt, sind die AtomicXXX-Klassen nicht optimal, und Java 8 bietet deshalb im Paket java.util.concurrent.atomic zwei neue Klassen LongAdder und DoubleAdder an. Ein XXXAdder sieht nach außen wie ein long/double aus (die Klassen erweitern auch Number), doch intern ist es vielmehr eine Liste von Werten, auf die dann unterschiedliche Threads zugreifen können, ohne zu warten. Um sich das vorstellen zu können, ein Beispiel: Nehmen wir an, mehrere Threads teilen sich einen LongAdder. Ruft ein Thread add(1) auf, so führt das intern zu einem Element in einer Liste.[ 37 ](Genau genommen beim ersten Element noch in einer Variablen, die Liste beginnt erst beim zweiten Element, also beim zweiten parallelen Thread.) Kommt gleichzeitig add(2) am LongAdder an, muss der Thread nicht auf das Ende vom ersten add(…) warten, sondern fügt einen neuen Knoten an. Kommen später ein dritter und vierter Thread über den Weg und führen add(3) und add(4) aus, können diese ohne zu warten an den ersten und zweiten existierenden Knoten gehen und die Werte addieren, in den beiden internen Knoten stehen also 4 und 6. Es sind also nur so viele Knoten intern nötig, wie wirklich parallele Threads auftauchen. Eine Summation am Ende mit sum() läuft dann über die internen Knoten und summiert sie auf zu 10, was 1+2+3+4 ist.

LongAccumulator und DoubleAccumulator

Die beiden XXXAdder-Klassen haben eine ganz spezielle Aufgabe, und die ist, Werte zu addieren und aufzusummieren. Allerdings gibt es noch viele weitere Aufgaben, die ähnlich wie die Summation auf ein Endergebnis gebracht werden. Für diesen Anwendungsfall deklariert das Paket java.util.concurrent.atomic weiterhin LongAccumulator und DoubleAccumulator. Im Konstruktor nehmen die Klassen einen XXXBinaryOperator an und die Identität, die beim binären Operator auf nur einem Ergebnis genau das Ergebnis gibt.

  • LongAccumulator(LongBinaryOperator accumulatorFunction, long identity)

  • DoubleAccumulator(DoubleBinaryOperator accumulatorFunction, double identity)

Die Methoden heißen dann accumulate(long x) bzw. accumulate(double x), und get() verdichtet das Ergebnis zum Schluss. Die Addition der Klassen LongAdder und DoubleAdder lässt sich dann alternativ ausdrücken durch new XXXXAccumulator((x, y) -> x + y, 0).

 


Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java SE 8 Standard-Bibliothek Java SE 8 Standard-Bibliothek
Jetzt Buch bestellen

 Buchempfehlungen
Zum Rheinwerk-Shop: Java ist auch eine Insel
Java ist auch eine Insel


Zum Rheinwerk-Shop: Professionell entwickeln mit Java EE 8
Professionell entwickeln mit Java EE 8


Zum Rheinwerk-Shop: Besser coden
Besser coden


Zum Rheinwerk-Shop: Entwurfsmuster
Entwurfsmuster


Zum Rheinwerk-Shop: IT-Projektmanagement
IT-Projektmanagement


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2018. Original - https://www.rheinwerk-verlag.de/openbook/
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.

 

 
 


29.09.2022 - Sitemap.xml