Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
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 4 Datenstrukturen und Algorithmen
Pfeil 4.1 Datenstrukturen und die Collection-API
Pfeil 4.1.1 Designprinzip mit Schnittstellen, abstrakten und konkreten Klassen
Pfeil 4.1.2 Die Basisschnittstellen Collection und Map
Pfeil 4.1.3 Die Utility-Klassen Collections und Arrays
Pfeil 4.1.4 Das erste Programm mit Container-Klassen
Pfeil 4.1.5 Die Schnittstelle Collection und Kernkonzepte
Pfeil 4.1.6 Schnittstellen, die Collection erweitern, und Map
Pfeil 4.1.7 Konkrete Container-Klassen
Pfeil 4.1.8 Generische Datentypen in der Collection-API
Pfeil 4.1.9 Die Schnittstelle Iterable und das erweiterte for
Pfeil 4.2 Listen
Pfeil 4.2.1 Erstes Listen-Beispiel
Pfeil 4.2.2 Auswahlkriterium ArrayList oder LinkedList
Pfeil 4.2.3 Die Schnittstelle List
Pfeil 4.2.4 ArrayList
Pfeil 4.2.5 LinkedList
Pfeil 4.2.6 Der Feld-Adapter Arrays.asList(…)
Pfeil 4.2.7 ListIterator *
Pfeil 4.2.8 toArray(…) von Collection verstehen – die Gefahr einer Falle erkennen
Pfeil 4.2.9 Primitive Elemente in Datenstrukturen verwalten
Pfeil 4.3 Mengen (Sets)
Pfeil 4.3.1 Ein erstes Mengen-Beispiel
Pfeil 4.3.2 Methoden der Schnittstelle Set
Pfeil 4.3.3 HashSet
Pfeil 4.3.4 TreeSet – die sortierte Menge
Pfeil 4.3.5 Die Schnittstellen NavigableSet und SortedSet
Pfeil 4.3.6 LinkedHashSet
Pfeil 4.4 Queues (Schlangen) und Deques
Pfeil 4.4.1 Queue-Klassen
Pfeil 4.4.2 Deque-Klassen
Pfeil 4.4.3 Blockierende Queues und Prioritätswarteschlangen
Pfeil 4.4.4 PriorityQueue
Pfeil 4.5 Stack (Kellerspeicher, Stapel)
Pfeil 4.5.1 Die Methoden von java.util.Stack
Pfeil 4.6 Assoziative Speicher
Pfeil 4.6.1 Die Klassen HashMap und TreeMap
Pfeil 4.6.2 Einfügen und Abfragen des Assoziativspeichers
Pfeil 4.6.3 Über die Bedeutung von equals(…) und hashCode() bei Elementen
Pfeil 4.6.4 Eigene Objekte hashen
Pfeil 4.6.5 LinkedHashMap und LRU-Implementierungen
Pfeil 4.6.6 IdentityHashMap
Pfeil 4.6.7 Das Problem veränderter Elemente
Pfeil 4.6.8 Aufzählungen und Ansichten des Assoziativspeichers
Pfeil 4.6.9 Die Arbeitsweise einer Hash-Tabelle *
Pfeil 4.6.10 Die Properties-Klasse
Pfeil 4.7 Mit einem Iterator durch die Daten wandern
Pfeil 4.8 Iterator-Schnittstelle
Pfeil 4.8.1 Der Iterator kann (eventuell auch) löschen
Pfeil 4.8.2 Operationen auf allen Elementen durchführen
Pfeil 4.8.3 Einen Zufallszahlen-Iterator schreiben
Pfeil 4.8.4 Iteratoren von Sammlungen, das erweiterte for und Iterable
Pfeil 4.8.5 Fail-Fast-Iterator und die ConcurrentModificationException
Pfeil 4.8.6 Die Schnittstelle Enumerator *
Pfeil 4.9 Algorithmen in Collections
Pfeil 4.9.1 Die Bedeutung von Ordnung mit Comparator und Comparable
Pfeil 4.9.2 Sortieren
Pfeil 4.9.3 Den größten und kleinsten Wert einer Collection finden
Pfeil 4.9.4 Nichtänderbare Datenstrukturen, immutable oder nur lesen?
Pfeil 4.9.5 Null Object Pattern und leere Sammlungen/Iteratoren zurückgeben
Pfeil 4.9.6 Echte typsichere Container
Pfeil 4.9.7 Mit der Halbierungssuche nach Elementen fahnden
Pfeil 4.9.8 Ersetzen, Kopieren, Füllen, Umdrehen, Rotieren *
Pfeil 4.9.9 Listen durchwürfeln *
Pfeil 4.9.10 Häufigkeit eines Elements *
Pfeil 4.9.11 Singletons *
Pfeil 4.9.12 nCopies(…) *
Pfeil 4.10 Datenstrukturen mit Änderungsmeldungen
Pfeil 4.10.1 Das Paket javafx.collections
Pfeil 4.10.2 Fabrikmethoden in FXCollections
Pfeil 4.10.3 Änderungen melden über InvalidationListener
Pfeil 4.10.4 Änderungen melden über XXXChangeListener
Pfeil 4.10.5 Change-Klassen
Pfeil 4.10.6 Weitere Hilfsmethoden einer ObservableList
Pfeil 4.10.7 Melden von Änderungen an Arrays
Pfeil 4.10.8 Transformierte FXCollections
Pfeil 4.10.9 Weitere statische Methoden in FXCollections
Pfeil 4.11 Stream-API
Pfeil 4.11.1 Stream erzeugen
Pfeil 4.11.2 Terminale Operationen
Pfeil 4.11.3 Intermediäre Operationen
Pfeil 4.11.4 Streams mit primitiven Werten
Pfeil 4.11.5 Stream-Beziehungen, AutoCloseable
Pfeil 4.11.6 Stream-Builder
Pfeil 4.11.7 Spliterator
Pfeil 4.11.8 Klasse StreamSupport
Pfeil 4.12 Spezielle threadsichere Datenstrukturen
Pfeil 4.12.1 Zu Beginn nur synchronisierte Datenstrukturen in Java 1.0
Pfeil 4.12.2 Nicht synchronisierte Datenstrukturen in der Standard-Collection-API
Pfeil 4.12.3 Nebenläufiger Assoziativspeicher und die Schnittstelle ConcurrentMap
Pfeil 4.12.4 ConcurrentLinkedQueue
Pfeil 4.12.5 CopyOnWriteArrayList und CopyOnWriteArraySet
Pfeil 4.12.6 Wrapper zur Synchronisation
Pfeil 4.12.7 Blockierende Warteschlangen
Pfeil 4.12.8 ArrayBlockingQueue und LinkedBlockingQueue
Pfeil 4.12.9 PriorityBlockingQueue
Pfeil 4.12.10 Transfer-Warteschlangen – TransferQueue und LinkedTransferQueue
Pfeil 4.13 Google Guava (Google Collections Library)
Pfeil 4.13.1 Beispiel Multi-Set und Multi-Map
Pfeil 4.13.2 Datenstrukturen aus Guava
Pfeil 4.13.3 Utility-Klassen von Guava
Pfeil 4.13.4 Prädikate
Pfeil 4.13.5 Transformationen
Pfeil 4.14 Die Klasse BitSet für Bitmengen *
Pfeil 4.14.1 Ein BitSet anlegen
Pfeil 4.14.2 BitSet füllen und Zustände erfragen
Pfeil 4.14.3 Mengenorientierte Operationen
Pfeil 4.14.4 Weitere Methoden von BitSet
Pfeil 4.14.5 Primzahlen in einem BitSet verwalten
Pfeil 4.15 Zum Weiterlesen
 
Zum Seitenanfang

4.12Spezielle threadsichere Datenstrukturen Zur vorigen ÜberschriftZur nächsten Überschrift

In Zeiten der Kernexplosion – jedes moderne Handy hat heute schon mindestens zwei CPU-Kerne – ändert sich auch die Programmierung grundlegend. Datenstrukturen, auf die Programme nebenläufig zugreifen, müssen auf diese Situation vorbereitet sein, denn andernfalls gehen Daten verloren, tauchen doppelt auf, oder die Datenstruktur ist komplett unbrauchbar.

 
Zum Seitenanfang

4.12.1Zu Beginn nur synchronisierte Datenstrukturen in Java 1.0 Zur vorigen ÜberschriftZur nächsten Überschrift

Als Java geboren wurde, gab es im Grunde nur zwei Datenstrukturen: Vector und Hashtable. Es gab keine Mengen oder Queues, wer die benötigte, musste die Funktionalität mit Vector und Hashtable realisieren oder neu programmieren. Das java.util-Paket enthält mit Stack und Properties noch zwei weitere Datenstrukturen, doch Stack ist eine simple Unterklasse von Vector und Properties eine einfache Unterklasse von Hashtable, sodass sich die Implementierungsdetails von Vector/Hashtable auf die Unterklassen übertragen.

Eine wichtige Eigenschaft der Klassen Vector und Hashtable ist, dass sie synchronisiert sind. Das bedeutet, dass ein Thread, der eine Methode aufruft, damit allen anderen Threads so lange den Zutritt verwehrt, bis die Operation angeschlossen ist. Erst dann öffnet sich das Tor wieder für andere Methoden.

Eine Dauersynchronisation klingt zwar nett, hat aber zwei Nachteile: Zum einen kostete insbesondere zu Anfangszeiten der virtuellen Maschinen die Steuerung der Zugriffe Geschwindigkeit, und zum anderen muss bedacht werden, dass einige Operationen ja grundsätzlich schon parallel ohne Synchronisation abgearbeitet werden können, insbesondere alle Leseoperationen.

 
Zum Seitenanfang

4.12.2Nicht synchronisierte Datenstrukturen in der Standard-Collection-API Zur vorigen ÜberschriftZur nächsten Überschrift

Bei den aktuellen Klassen wie ArrayList, TreeSet oder HashMap sind die Methoden nicht mehr automatisch synchronized. Nutzen Programme diese Datenstrukturen nicht nebenläufig, gibt es folglich auch keinen Geschwindigkeitsverlust. Allerdings müssen Entwickler sich nun selbst um eine korrekte Sperrung kümmern.

Sollen Listen, Mengen oder Assoziativspeicher vor nebenläufigen Änderungen sicher sein, gibt es zwei Möglichkeiten: einmal über spezielle so genannte Wait-free- bzw. Lock-free-Algorithmen, die tatsächlich parallele Zugriffe – wenn möglich – ohne Lock erlauben, und einmal über synchronisierende Wrapper.

Wait-free-Algorithmen

Wenn zum Beispiel bei einer verketteten Liste ein Thread vorn etwas anhängt und der andere hinten etwas entfernt, ist das tatsächlich nebenläufig möglich, und es muss nicht die ganze Datenstruktur gelockt werden. Auch bei anderen Datenstrukturen kann direkt ohne Lock eine Operation durchgeführt werden, und es muss nur im Spezialfall eine besondere Absicherung vorgenommen werden.

Hierfür gibt es neue Klassen, die nicht im Paket java.util liegen, sondern in einem Unterpaket, java.util.concurrent. So lässt sich leicht merken: Bis auf Vector und Hashtable sind alle Datenstrukturen in java.util nicht threadsicher, alle in java.util.concurrent schon.

 
Zum Seitenanfang

4.12.3Nebenläufiger Assoziativspeicher und die Schnittstelle ConcurrentMap Zur vorigen ÜberschriftZur nächsten Überschrift

Die Schnittstelle java.util.concurrent.ConcurrentMap erweitert die Schnittstelle java.util.Map, führt aber keine Methoden hinzu, sondern dokumentiert nur das Verhalten von elf Operationen, dass diese threadsicher und atomar sind. Die Implementierungen der Schnittstelle sind:

  • ConcurrentHashMap: Ein schneller threadsicherer Assoziativspeicher nach dem Hashing-Verfahren. Seit Java 8 gibt es über 30 neue Methoden, die insbesondere die funktionale Programmierung unterstützen. Auch neu in Java 8 ist die Rückgabe von keySet(), die nun nicht mehr Set<K> ist, sondern ConcurrentHashMap.KeySetView<K,V>, eine besondere Set-Implementierung; hier nutzt Java 8 also kovariante Rückgabetypen.

  • ConcurrentSkipListMap: Ein threadsicherer Assoziativspeicher, der automatisch sortiert ist, also vergleichbar mit TreeMap. Die Klasse implementiert ConcurrentNavigableMap (die wiederum NavigableMap erweitert), eine Schnittstelle, die Methoden zur Teilmengenbildung vorgibt.

Beide sind sehr schnelle Datenstrukturen für gleichzeitig operierende Threads, die dann auch keine ConcurrentModificationException auslösen, wenn es parallele Veränderungen über einen Iterator gibt.

 
Zum Seitenanfang

4.12.4ConcurrentLinkedQueue Zur vorigen ÜberschriftZur nächsten Überschrift

Obwohl es keine Schnittstellen ConcurrentSet und ConcurrentList gibt, existiert zumindest eine Klasse ConcurrentLinkedQueue, die eine threadsichere und wartefreie Liste (genauer Queue) ist. Wie der Name schon andeutet, beruht die Realisierung auf verketteten Listen und nicht auf Arrays. Ein eigenes ConcurrentSet könnte auf der Basis von ConcurrentHashMap implementiert werden, so wie auch HashSet intern mit einer HashMap realisiert ist. Die Klasse ConcurrentSkipListSet ist eine performante nebenläufige Implementierung der Schnittstelle NavigableSet.

 
Zum Seitenanfang

4.12.5CopyOnWriteArrayList und CopyOnWriteArraySet Zur vorigen ÜberschriftZur nächsten Überschrift

Ist die Anzahl der Leseoperationen hoch, kann es sich lohnen, bei jedem Schreibzugriff erst die Daten zu kopieren und dann das Element hinzuzufügen, damit im Hintergrund andere Threads ohne einen Lock, der für das Schreiben nötig ist, Daten lesen können. Zwei dieser Datenstrukturen bietet Java an: CopyOnWriteArrayList für Listen und CopyOnWriteArraySet für Mengen. Die Klassen sind genau dann optimal, wenn wenig verändert – das ist teuer – und fast ausschließlich gelesen wird. Auch führen die Klassen zu keiner ConcurrentModificationException beim Ändern und gleichzeitigen Ablauf über Iteratoren. Denn falls die CopyOnWriteXXX-Datenstruktur verändert wird, arbeiten die »alten« Interessenten ja mit der herkömmlichen Version. Wenn zum Beispiel ein Iterator durch die Daten läuft und es dann eine Änderung gibt, so führt die Änderung zu einer Kopie der Daten und zur anschließenden Veränderung. Kommt eine Anfrage an die Datenstruktur nach der Veränderung, so bekommt der Anfrager Daten aus der neuen veränderten Datenstruktur. Kommt eine Anfrage während der Veränderung, also zu dem Zeitpunkt, an dem die Veränderung noch nicht abgeschlossen ist, so ist weiterhin die alte Version die einzig korrekte, und Daten kommen aus dieser CopyOnWriteXXX-Datenstruktur.

 
Zum Seitenanfang

4.12.6Wrapper zur Synchronisation Zur vorigen ÜberschriftZur nächsten Überschrift

Können zur Absicherung nebenläufiger Operationen die oben aufgeführten Datenstrukturen aus java.util.concurrent nicht verwendet werden, etwa bei Java 1.4 oder bei eigenen, nicht nebenläufig implementierten Versionen von Set, Map, List und Queue, lassen sich Zugriffe auf die Datenstrukturen extern synchronisieren. Dazu fordern statische Methoden wie synchronizedXXX(…) einen Wrapper an, der sich um die existierende Datenstruktur legt. Die Wrapper arbeiten mit einem Lock, und Parallelität in den Datenstrukturen ist nicht gegeben.

[zB]Beispiel

Eine synchronisierte Liste:

List<Byte> syncList = Collections.synchronizedList( new ArrayList<>() );

Der generische Typ der Datenstruktur geht auch weiter an den Wrapper.

Die statischen synchronizedXXX(…)-Methoden liefern eine neue Sammlung, die sich wie eine Hülle um die existierende Datenstruktur legt und alle Methodenaufrufe synchronisiert. Paralleler Zugriff darf natürlich dann nur noch über den Wrapper laufen, wobei nichtparalleler Zugriff weiterhin über die Original-Datenstruktur möglich ist.

class java.util.Collections
  • static <T> Collection<T> synchronizedCollection(Collection<T> c)

  • static <T> List<T> synchronizedList(List<T> list)

  • static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)

  • static <T> Set<T> synchronizedSet(Set<T> s)

  • static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)

  • static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)

  • static <T> NavigableSet<T> synchronizedNavigableSet(NavigableSet<T> s)
    Neu in Java 8.

  • static <K,V> NavigableMap<K,V> synchronizedNavigableMap(NavigableMap<K,V> m)
    Neu in Java 8. Liefert synchronisierte, also threadsichere Datenstrukturen.

[»]Hinweis zu Iteratoren von synchronisierten Wrappern

Mit dem Wrapper ist der nebenläufige Zugriff über die Methoden gesichert, aber nicht der Zugriff über den Iterator. Ist syncList eine synchronisierte Datenstruktur, die ein Iterator ablaufen möchte und soll während des Ablaufens jeder andere Zugriff gesperrt sein, so ist Folgendes zu schreiben:

List<Byte> syncList = Collections.synchronizedList( new ArrayList<>() );
synchronized ( syncList ) {
Iterator<Byte> iter = syncList.iterator();
}

Die Synchronisation ist immer auf dem Wrapper und nicht auf dem »Gewrappten«.

 
Zum Seitenanfang

4.12.7Blockierende Warteschlangen Zur vorigen ÜberschriftZur nächsten Überschrift

Die Schnittstelle BlockingQueue steht für besondere Queues, die blockieren können. Das kann aus zwei Gründen geschehen: Entweder sind keine Daten zu entnehmen, da die Queue leer ist, oder eine maximale Anzahl von zu haltenden Elementen ist erreicht. Besonders in Produzenten-/Konsumenten-Szenarien sind blockierende Warteschlangen sehr nützlich.

Eine performante threadsichere Implementierung ist in der Praxis sehr wichtig, und die Java SE-Bibliothek hat zwei besondere Realisierungen auf Lager:

  • ArrayBlockingQueue: Queue immer mit einer maximalen Kapazität, intern realisiert mit einem Feld

  • LinkedBlockingQueue: Queue unbeschränkt oder mit maximaler Kapazität, intern realisiert durch eine verkettete Liste

  • PriorityBlockingQueue: eine blockierende PriorityQueue

Die anderen Realisierungen wie DelayQueue sind für uns jetzt nicht relevant.

Das Schöne an blockierenden Warteschlangen ist ihr Verhalten in Produzenten-/Konsumenten-Verhältnissen; ein Thread (oder beliebig viele Threads) setzt (viele setzen) Daten in die Queue, ein Thread (oder beliebig viele Threads) holt (viele Threads holen) die Daten wieder raus. Bei einer Queue ist es ja so, dass sie nach dem FIFO-Verfahren arbeitet, das heißt, die Daten, die zuerst hineingelegt wurden, werden auch zuerst verarbeitet. Bei einer Prioritätswarteschlange ist das etwas anders, aber dazu gleich mehr.

 
Zum Seitenanfang

4.12.8ArrayBlockingQueue und LinkedBlockingQueue Zur vorigen ÜberschriftZur nächsten Überschrift

Bei den normalen blockierenden Datenstrukturen geht es darum, sich zwischen ArrayBlockingQueue und LinkedBlockingQueue zu entscheiden. Der wesentliche Unterschied ist – bis auf die interne Realisierung – dass die ArrayBlockingQueue immer mit einer maximalen Schranke von Elementen konfiguriert werden muss; ist diese Schranke erreicht, blockiert die Queue und nimmt keine neuen Elemente mehr an. Die LinkedBlockingQueue kann unbegrenzt wachsen (also bis Integer.MAX_VALUE). Ist die Queue dann offen, wird nur geblockt, wenn kein Element vorhanden ist, ein Thread aber eines entnehmen möchte.

Beispiel mit unbeschränkter LinkedBlockingQueue

Dazu ein einfaches Beispiel: Wir haben zwei Produzenten für Meldungen und einen Konsumenten, der sie nach Eingang einfach auf der Konsole ausgibt:

Listing 4.39com/tutego/insel/thread/concurrent/LoggingInQueue.java, LoggingInQueue

public class LoggingInQueue {

private static final BlockingQueue<String> messages =
new LinkedBlockingQueue<>();

private static class MessageOutputter extends Thread {
@Override public void run() {
while ( true )
try {
long startTime = System.currentTimeMillis();
System.out.printf( "%s (Wartete %d ms)%n",
messages.take(),
System.currentTimeMillis() – startTime ); }
catch ( InterruptedException e ) { }
}
}

private static class UserMessageProducer extends Thread {
@Override public void run() {
for( int i = 0; ; i++ )
messages.add( "msg " + i + " " +
JOptionPane.showInputDialog( "Meldung eingeben" ) );
}
}

private static class DiskspaceMessageProducer extends Thread {
@Override public void run() {
for( int i = 0; ; i++ ) {
String dir = System.getProperty( "user.dir" );
messages.add( "spc " + i + " " + new File( dir ).getFreeSpace() );
try { TimeUnit.SECONDS.sleep( 1 ); }
catch ( InterruptedException e ) { }
}
}
}

public static void main( String[] args ) {
new MessageOutputter().start();
new UserMessageProducer().start();
new DiskspaceMessageProducer().start();
}
}

Auf der einen Seite steht der Thread, der die blockiere Queue abhorcht. Wir fragen die Daten mit take() ab, damit die Methode – und somit der Thread – blockiert, falls keine Daten in der Queue sind. Die BlockingQueue-Schnittstelle definiert auch noch andere Methoden wie poll() oder peek(), aber die blockieren nicht, sondern liefern null, wenn keine Daten in der Queue sind – eigentlich sind die Methoden unpassend in einer blockierenden Datenstruktur, aber BlockingQueue erbt diese Methoden von Queue. (Leser sollten zum Test take() durch poll() ersetzen und das Ergebnis testen.) Um eine Vorstellung zu bekommen, wie lange take() warten muss, holen wir über System.currentTimeMillis() die Systemzeit in Millisekunden relativ zum 1.1.1970 vor dem Aufruf von take() sowie nach dem Aufruf von take() – die Differenz der Zeiten gibt uns eine Idee von der Dauer, während der der Thread wartet und auch keine Rechenzeit verbraucht.

Die Produzenten sind in unserem Fall zwei Threads. Der eine holt permanent die Anzahl freier Bytes auf der Festplatte des Benutzers, der andere Thread offeriert einen Benutzerdialog, und die Benutzereingabe kommt auf den Schirm. Gestartet mit ein paar Eingaben ist die Ausgabe dann auch recht unspektakulär:

lstspc 0 55943192576 (Wartete 11 ms)
spc 1 55942631424 (Wartete 943 ms)
spc 2 55943192576 (Wartete 1005 ms)
spc 3 55943192576 (Wartete 1007 ms)
msg 0 eingabe (Wartete 716 ms)
spc 4 55943192576 (Wartete 290 ms)
msg 1 eingabe (Wartete 822 ms)
spc 5 55943192576 (Wartete 180 ms)
msg 2 eingabe (Wartete 112 ms)
 
Zum Seitenanfang

4.12.9PriorityBlockingQueue Zur vorigen ÜberschriftZur nächsten Überschrift

Eine blockierende Prioritätswarteschlange ist eine besondere Prioritätswarteschlange, die threadsicher ist und den Entnehmer-Thread blockiert, wenn kein Element vorhanden ist, also die Queue leer ist. Für blockierende Prioritätswarteschlangen bietet Java eine Klasse: PriorityBlockingQueue; sie implementiert PriorityQueue. Die Sortierung ist entweder natürlich oder über einen externen Comparator geregelt, bei Letzterem muss das Vergleichsobjekt im Konstruktor übergeben werden.

Tickets mit Priorität

Unser nächstes Beispiel legt Tickets in eine blockierende Prioritätswarteschlange. Tickets mit der höchsten Priorität sollen nach vorne wandern. Gleichzeitig messen wir die Zeit beim Anlegen des Tickets, um bei Tickets mit gleicher Priorität dem Ticket den Vorzug zu geben, das als Erstes angelegt wurde.

Listing 4.40com/tutego/insel/thread/concurrent/Ticket.java

package com.tutego.insel.util.concurrent;

import java.util.Date;

class Ticket implements Comparable<Ticket> {

enum Priority { SEVERE, NORMAL }

private final Priority priority;
private final String message;
private final Date arrivalDate;

Ticket( Priority priority, String message ) {
this.priority = priority;
this.message = message;
arrivalDate = new Date();
}

@Override public int compareTo( Ticket that ) {
int ticketPriority = this.priority.compareTo( that.priority );
// Wenn Ticket-Priorität ungleich 0, dann ist ein Ticket wichtiger als das
// andere und die Zeit spielt keine Rolle.
if ( ticketPriority != 0 )
return ticketPriority;
// Wenn Ticket-Priorität gleich 0, dann sind beide Tickets
// gleich wichtig. Die Zeit kommt dann als Kriterium hinzu.
return this.arrivalDate.compareTo( that.arrivalDate );
}

@Override public String toString() {
return String.format( "%tT.%1$TL (%s) kam '%s'",
arrivalDate, priority, message );
}
}

Die Ticket-Klasse implementiert Comparable, sodass es eine natürliche Ordnung der Elemente gibt.

Ein Test, der die Ordnung bei der Sortierung zeigt, ist schnell geschrieben:

Listing 4.41com/tutego/insel/thread/concurrent/TicketDemo.java, main()

List<Ticket> tickets = Arrays.asList(
new Ticket( Priority.NORMAL, "Kein Senf" ),
new Ticket( Priority.SEVERE, "Feuer" ),
new Ticket( Priority.NORMAL, "Bier warm" ),
new Ticket( Priority.SEVERE, "Erdbeben" ) );
Collections.sort( tickets );
System.out.println( tickets );

Die Ausgabe ist wie:

13:45:20.777 (SEVERE) kam 'Feuer'
13:45:20.777 (SEVERE) kam 'Erdbeben'
13:45:20.777 (NORMAL) kam 'Kein Senf'
13:45:20.777 (NORMAL) kam 'Bier warm'

Die Ausgabe macht zwei Dinge deutlich: Zunächst, dass die wichtigen Meldungen vorne liegen, und dann als zweites, dass zuerst eingefügte Meldungen auch zeitlich vor den nachfolgenden Meldungen liegen (wobei das Programm so schnell ist, dass die Zeit gar nicht als Komponente berücksichtigt werden kann).

Tickets generieren und mit der blockierenden Warteschlange verarbeiten

Im letzten Schritt können wir zwei Threads starten, wobei ein Thread neue Tickets (mit zufälliger Wichtigkeit) in die Queue legt und ein anderer Thread die Nachrichten permanent entnimmt:

Listing 4.42com/tutego/insel/thread/concurrent/PrioritizedTicketsQueue.java

package com.tutego.insel.util.concurrent;

import java.util.concurrent.*;
import com.tutego.insel.util.concurrent.Ticket.Priority;

public class PrioritizedTicketsQueue {

private static final BlockingQueue<Ticket> tickets =
new PriorityBlockingQueue<>();

private static class TicketProducer extends Thread {
@Override public void run() {
while ( true ) {
Ticket ticket = new Ticket(
Priority.values()[ (int)(Math.random() * 2) ], "Hilfe!" );
tickets.add( ticket );
System.out.println( "Neues Ticket: " + ticket );
try { TimeUnit.MILLISECONDS.sleep( (int)(Math.random() * 2000) ); }
catch ( InterruptedException e ) { /* Empty */ }
}
}
}

private static class TicketSolver extends Thread {
@Override public void run() {
while ( true ) {
try {
System.out.println( tickets.take() );
TimeUnit.SECONDS.sleep( 1 );
}
catch ( InterruptedException e ) { /* Empty */ }
}
}
}

public static void main( String[] args ) {
new TicketProducer().start();
new TicketSolver().start();
}
}

Lassen wir das Programm starten, kann die Ausgabe wie folgt sein:

Neues Ticket: 14:28:55.422 (SEVERE) kam 'Hilfe!'
14:28:55.422 (SEVERE) kam 'Hilfe!'
Neues Ticket: 14:28:56.038 (NORMAL) kam 'Hilfe!'
Neues Ticket: 14:28:56.445 (NORMAL) kam 'Hilfe!'
14:28:56.038 (NORMAL) kam 'Hilfe!'
Neues Ticket: 14:28:57.213 (NORMAL) kam 'Hilfe!'
Neues Ticket: 14:28:57.264 (NORMAL) kam 'Hilfe!'
Neues Ticket: 14:28:57.443 (SEVERE) kam 'Hilfe!'
14:28:57.443 (SEVERE) kam 'Hilfe!'
14:28:56.445 (NORMAL) kam 'Hilfe!'
Neues Ticket: 14:28:58.618 (SEVERE) kam 'Hilfe!'
Neues Ticket: 14:28:59.025 (SEVERE) kam 'Hilfe!'
14:28:58.618 (SEVERE) kam 'Hilfe!'
Neues Ticket: 14:29:00.418 (SEVERE) kam 'Hilfe!'
14:28:59.025 (SEVERE) kam 'Hilfe!'

Interessant ist die fett gedruckte Zeile, denn sie zeigt deutlich, dass – obwohl zwei Tickets vorher generiert wurden – diese durch die spätere Meldung verdrängt werden, da die spätere Meldung eine höhere Wichtigkeit hat als die beiden Meldungen zuvor.

 
Zum Seitenanfang

4.12.10Transfer-Warteschlangen – TransferQueue und LinkedTransferQueue Zur vorigen ÜberschriftZur nächsten Überschrift

Setzen Produzenten etwas in eine Queue, so ist das in der Regel eine Ablegeoperation, die entkoppelt von der Entnahme ist. Vielleicht muss ein Ableger noch warten, wenn die Queue voll ist, aber sobald Platz ist, kehrt ein Geber-Thread sofort zurück. Ob ein Konsument vorhanden ist, spielt erst einmal keine Rolle.

Eine Unterschnittstelle von BlockingQueue ist TransferQueue. Sie deklariert neue Transfermethoden, die direkt Daten vom Produzenten zum wartenden Konsumenten bringen bzw. melden, wenn es keinen wartenden Thread gibt. Anders als bei den anderen Queues wächst bei den Transfermethoden die Queue nicht, denn ein xxxTransfer(…) ist kein add(…). Die Transfermethoden realisieren immer einen direkten Weg von einem Produzenten zu einem Konsumenten, der etwa mit take() schon auf Daten wartet. Insgesamt deklariert die Schnittstelle drei Übertragungsmethoden:

  • void transfer(E e) throws InterruptedException

  • boolean tryTransfer(E e)

  • boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException

Die erste Methode versucht eine Übermittlung, und gibt es keinen wartenden Konsumenten, blockiert transfer(…) so lange, bis das Datum abgeholt ist. Die zweite Methode ist ungeduldiger, denn gibt es keinen Konsumenten, wartet tryTransfer(..) nicht, sondern kehrt direkt mit der Rückgabe false zurück. Die zweite tryTransfer(…)-Variante bietet ein Zeitintervall, in dem ein Konsument die Daten abholen muss.

Die Java SE bietet bisher nur eine realisierende Klasse der Schnittstelle: LinkedTransferQueue.

 


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

 
Nutzungsbestimmungen | Datenschutz | Impressum

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

Cookie-Einstellungen ändern