1. Nebenläufige Programmierung mit Threads
Ein Betriebssystem bietet Threads als Programmierhilfe an, wovon Programme fleißig Gebrauch machen. Wer unter Windows arbeitet und einmal in den Task-Manager schaut, wird feststellen, dass schnell einmal 3000 Threads laufen. In diesem Kapitel wollen wir die Anzahl noch einmal hochschrauben, um komfortabel mehrere Handlungsstränge nebenläufig oder sogar parallel ausführen zu lassen. Dabei soll es bei den Aufgaben nicht einfach nur darum gehen, Threads zu erzeugen, sondern wenn etwas nebenläufig ist, muss auch auf den korrekten Zugriff auf gemeinsame Ressourcen geachtet werden — die Threads müssen sich koordinieren.
Voraussetzungen
main
-Thread nutzen könneneigene Threads erzeugen können, Unterschied zwischen
Thread
undRunnable
kennenThreads schlafen legen können
Threads unterbrechen können
Aufgaben in den Thread-Pool zur Bearbeitung abgeben können
nebenläufige Operationen mit Rückgaben einsetzen können
Unterschied zwischen
Runnable
undCallable
verstehenThreads mit
Lock
-Objekten synchronisieren könnenThreads untereinander mit
Condition
-Objekten benachrichtigen könnenSynchronisationshelfer wie
Semaphore
kennen
Verwendete Datentypen in diesem Kapitel:
Noch mehr Aufgaben findest du im Buch: ›Captain CiaoCiao erobert Java: Das Trainingsbuch für besseres Java. 300 Java-Workshops, Aufgaben und Übungen mit kommentierten Lösungen‹
1.1. Anlegen von Threads
Startet die JVM, erzeugt sie einen Thread mit dem Namen main
. Dieser Thread führt die main(…)
-Methode aus und er hat in allen vorherigen Aufgaben unsere Programme ausgeführt. Das wollen wir in den nächsten Aufgaben ändern. Wir wollen weitere Threads erzeugen und diese Programmcode abarbeiten lassen.
Thread
1.1.1. Threads erzeugen für Winken und Fähnchen schwenken ⭐
Zu Ehren vom Captain CiaoCiao gibt es eine Parade. Er steht an der Rampe seines Schiffs, winkt mit der einen Hand und schwenkt ein Fähnchen mit der anderen.
Aufgabe:
Threads führen in Java immer Dinge vom Typ
Runnable
aus.Runnable
ist eine funktionale Schnittstelle, und es gibt in Java zwei Möglichkeiten, funktionale Schnittstellen zu implementieren: Klassen und Lambda-Ausdrücke. Schreibe zwei Implementierungen vonRunnable
; einmal durch eine Klasse, einmal mit einem Lambda-Ausdruck.Setze in beide
Runnable
-Implementierungen eine Schleife mit fünfzig Wiederholungen. Das eineRunnable
soll auf dem Bildschirm »winken« ausgeben, die Ausgabe des anderen soll »Fähnchen schwenken« seinErzeuge ein
Thread
-Objekt, und übergib dasRunnable
. Starte dann die Threads. Starte nicht fünfzig Threads, sondern nur zwei!
Erweiterung: Die run()
-Methode jedes Threads soll die Zeile System.out.println(Thread.currentThread());
enthalten. Was wird dann angezeigt?
Nehmen wir an, Captain CiaoCiao hat noch ein paar mehr Arme zum Winken. Wie viele Threads lassen sich erzeugen, bis das System »steht«? Beobachte den Speicherverbrauch im Windows Task-Manager (Ctrl+Alt+Del). Lässt sich abschätzen, was ein Thread »kostet«?
1.1.2. Nix mehr mit Winken und Fähnchenschwenken: Threads beenden ⭐
Threads lassen sich hart mit stop()
beenden — die Methode ist seit Jahrzehnten deprecated
, aber wird wohl nie gelöscht — oder nett mit interrupt()
bitten, sich selbst zu beenden. Dafür muss der Thread aber mitspielen und mit isInterrupted()
prüfen, ob ein solcher Löschantrag für ihn vorliegt.
Captain CiaoCiao steht immer noch auf dem Schiff, winkt und schwenkt Fähnchen. Wenn es ernst wird, muss er diese Volksbelustigung beenden.
Aufgabe:
Schreibe ein Programm mit zwei
Runnable
-Implementierungen, die prinzipiell unendlich lange winken und Fähnchen schwenken, es sei denn, es gibt eine Unterbrechung. Dierun()
-Methode soll daher überThread.currentThread().isInterrupted()
testen, ob es eine Unterbrechung gab, und dann die Schleife verlassen.Baue in die Schleife eine Verzögerung ein. Kopiere dafür folgenden Code:
try { Thread.sleep( 2000 ); } catch ( InterruptedException e ) { Thread.currentThread().interrupt(); }
Das Hauptprogramm soll mit
JOptionPane.showInputDialog(String)
auf eine Eingabe reagieren, sodass die Kommandosendw
das Winken undendf
das Fähnchenschwenken beenden.
1.1.3. Runnable parametrisieren ⭐⭐
Der Blick auf den folgenden Code zeigt, dass die beiden Runnable
-Implementierungen sehr ähnlich sind und sich nur in der Bildschirmausgabe unterscheiden (Unterschiede in der run()
-Methode fett):
// Runnable 1
class Wink implements Runnable {
@Override public void run() {
for ( int i = 0; i < 50; i++ )
System.out.printf( "Wink; %s%n", Thread.currentThread() );
}
}
Runnable winker = new Wink();
// Runnable 2
Runnable flagWaver = () -> {
for ( int i = 0; i < 50; i++ )
System.out.printf( "Wave flag; %s%n", Thread.currentThread() );
};
Codeduplizierung ist aber selten gut, das sollte geändert werden.
Aufgabe:
Überlege, wie man einem
Runnable
Daten mitgeben kann.Implementiere ein parametrisiertes
Runnable
, sodass man in der oben genannten Schleifedie Bildschirmausgaben und
die Anzahl Wiederholungen frei bestimmen kann.
Schreibe das Winken-und-Fähnchenschwenken-Programm so um, dass das parametrisierte
Runnable
einem Thread zur Ausführung übergeben wird.
1.2. Ausgeführt und stillgestanden
Ein Thread kann sich in mehreren Zuständen befinden, dazu zählen laufend, wartend, schlafend, blockiert oder beendet. In den vorherigen Aufgaben haben wir den Thread gestartet, sodass er im Zustand laufend ist, und die run()
-Methode haben wir durch das Ende der Schleife beendet, was auch den Thread beendet. In diesem Abschnitt geht es in den Aufgaben um den Zustand schlafend, und im Abschnitt »Kritische Abschnitte schützen«, und »Thread-Kooperation und Synchronisationshelfer«, gibt es Aufgaben um die Zustände wartend/blockiert.
1.2.1. Abarbeitung durch schlafende Threads verzögern ⭐⭐
Das unter Unix bekannte Programm sleep (http://man7.org/linux/man-pages/man1/sleep.1.html) lässt sich von der Kommandozeile aufrufen und schläft dann eine Zeit lang, verzögert also in Skripten die nachfolgenden Programme.
Aufgabe:
Implementiere das sleep-Programm in Java neu, sodass man vergleichbar wie beim Vorbild auf der Kommandozeile schreiben kann:
$ java Sleep 22
Dann soll das Java-Programm 22 Sekunden schlafen und wenn es zum Beispiel in einem Skript nachfolgende Programmaufrufe gibt, werden diese verzögert.
Dem Java-Programm soll man die Schlafzeit auf verschiedene Arten auf der Kommandozeile mitgeben können. Wird nur eine Ganzzahl übergeben, dann steht die Wartezeit für Sekunden. Es sollen Suffixe hinter der Ganzzahl für unterschiedliche Dauern erlaubt sein:
s
für Sekunden (Standard)m
für Minutenh
für Stundend
für Tage
Werden mehrere Werte übergeben, so werden sie summiert und ergeben die Gesamtwartezeit.
Bei dem Aufruf können verschiedene Dinge schiefgehen, etwa wenn keine Zahl übergeben wird oder die Zahl zu groß ist. Prüfe, ob die Werte, Bereiche und Suffixe korrekt sind. Optional: Beende das Programm im Fehlerfall mit einem individuellen Exit-Code über
System.exit(int)
.
Beispiel:
Gültige Aufrufbeispiele:
$ java Sleep 1m $ java Sleep 1m 2s $ java Sleep 1h 3h 999999s
Ungültige Aufrufe, die zum Abbruch führen:
$ java Sleep $ java Sleep three $ java Sleep 1y $ java Sleep 9999999999999999999999
Tipp: Strukturiere das Programm so, dass die drei wesentlichen Teile erkennbar sind:
das Ablaufen der Kommandozeile und Analysieren der Übergaben
das Konvertieren der Einheiten in Sekunden
das eigentliche Schlafen, und zwar die akkumulierten Sekunden lang
1.2.2. Dateiänderungen beobachten durch Threads ⭐
Nach einer erfolgreichen Plünderung werden alle neuen Schätze systematisch in eine Inventarliste aufgenommen. Diese wird in einer einfachen Datei gespeichert. Bonny Brain möchte Bescheid bekommen, wenn sich die Datei ändert.
Aufgabe:
Schreibe eine Klasse
FileChangeWatcher
mit einem KonstruktorFileChangeWatcher(String filename)
.Implementiere
Runnable
.Gib im Abstand von einer halben Sekunde den Dateinamen, die Dateigröße und
Files.getLastModifiedTime(path)
aus.Überprüfe mit
getLastModifiedTime(…)
, ob sich die Datei geändert hat. Gib eine Meldung aus, wenn sich die Datei ändert. Das alles soll endlos geschehen, jede neue Änderung soll gemeldet werden.Erweiterung: Wir möchten nun flexibler auf Änderungen reagieren. Dazu soll im Konstruktor ein
java.util.function.Consumer
-Objekt übergeben werden können. Den Konsumenten soll sichFileChangeWatcher
merken und immer dann dieaccept(Path)
-Methode aufrufen, wenn sich etwas ändert. So können wir ein Objekt anmelden, das bei einer Dateiänderung informiert wird.
1.2.3. Exceptions auffangen ⭐
Die Unterscheidung zwischen geprüften Ausnahmen und ungeprüften Ausnahmen hat eine wichtige Konsequenz, denn ungeprüfte Ausnahmen können, falls nicht gefangen, so weit eskalieren, dass sie beim ausführenden Thread landen, der dann von der virtuellen Maschine beendet wird. Dies macht die Laufzeitumgebung automatisch, und wir bekommen freundlicherweise noch eine Meldung auf den Standardfehlerkanal, aber wiederbeleben können wir den Thread nicht mehr.
Auf einem lokalen Thread oder global für alle Threads lässt sich ein UncaughtExceptionHandler
installieren, der informiert wird, wenn der Thread durch eine Ausnahme beendet wird. Er lässt sich in vier Szenarien einsetzen:
Ein
UncaughtExceptionHandler
kann auf einem individuellen Thread gesetzt werden. Immer dann, wenn dieser Thread eine unbehandelte Ausnahme bekommt, wird der Thread abgebrochen und der gesetzteUncaughtExceptionHandler
informiert.Ein
UncaughtExceptionHandler
kann auf einer Thread-Gruppe gesetzt werden.Ein
UncaughtExceptionHandler
lässt sich global für alle Threads setzen.Der
main
-Thread ist insofern etwas Besonderes, als die JVM ihn automatisch anlegt und das Hauptprogramm ausführt. Natürlich kann es auch immain
-Thread ungeprüfte Ausnahmen geben, den einUncaughtExceptionHandler
melden kann. Allerdings kommt eine interessante Besonderheit dazu: An dermain(…)
-Methode kannthrows
stehen, und geprüfte Ausnahmen können so an die JVM zurückgehen. Im Fall einer geprüften Ausnahme wird ebenfalls ein gesetzterUncaughtExceptionHandler
benachrichtigt.
Die Abarbeitung findet in einer Kaskade statt: Gibt es eine ungeprüfte Ausnahme, schaut die JVM zunächst, ob ein UncaughtExceptionHandler
beim individuellen Thread gesetzt ist. Falls nicht, wird nach einem UncaughtExceptionHandler
bei der Thread-Gruppe geschaut und anschließend nach einem globalen Handler, der informiert wird.
Aufgabe:
Lasse einen Thread laufen, der durch die Division durch
0
beendet wird. Dokumentiere diese Ausnahme durch einen eigenen globalenUncaughtExceptionHandler
.Starte einen zweiten Thread, der einen lokalen
UncaughtExceptionHandler
hat, der die Ausnahme ignoriert, sodass auch keine Meldung erscheint.Wenn an der
main(…)
-Methodethrows Exception
und im Rumpfnew URL("captain")
steht, wird auch dann der globaleUncaughtExceptionHandler
aufgerufen?
1.3. Thread-Pools und Ergebnisse
Es ist nicht immer der beste Weg für Java-Entwickler, selbst Threads anzulegen und diese mit Programmcode zu verbinden; häufig ist es vernünftiger, den Programmcode von der physikalischen Ausführung zu trennen. Dies übernimmt in Java ein Executor
. Damit lässt sich der Programmcode vom eigentlichen Thread trennen und auch der gleiche Thread mehrmals für unterschiedlichen Programmcode einsetzen.
In der Java-Bibliothek gibt es drei zentrale Executor
-Implementierungen: ThreadPoolExecutor
, ScheduledThreadPoolExecutor
und ForkJoinPool
. Die Typen ThreadPoolExecutor
und ForkJoinPool
realisieren Thread-Pools die eine Sammlung von existierenden Threads verwalten, sodass Aufgaben an existierende, freie Threads übergeben werden können.
Jede Codeausführung im Hintergrund wird über einen Thread in Java realisiert, der entweder selbst erzeugt und gestartet, oder indirekt von einem Executor
oder internem Thread-Pool eingesetzt wird. Es gibt zwei wichtige Schnittstellen, um nebenläufigen Code zu kapseln: Runnable
und Callable
. Runnable
wird direkt dem Thread
-Konstruktor übergeben, ein Callable
kann man nicht bei Thread
übergeben; für Callable
benötigt man einen Executor
. Ein Callable
liefert zudem ein Ergebnis zurück, so wie es auch ein Supplier
tut, er hat aber keinen Parameter für eine Übergabe. Bei einem Runnable
lässt sich nichts zurückliefern und auch nicht übergeben. Die run()
-Methode löst keine Ausnahme aus, call()
trägt in der Methodensignatur throws Exception
, kann also beliebige Ausnahmen weiterleiten.
Runnable
und Callable
Bisher haben wir Threads immer selbst aufgebaut und nur Runnable
verwendet. In den folgenden Aufgaben wird es um Thread-Pools und auch um Callable
gehen.
1.3.1. Thread-Pool nutzen ⭐⭐
Ostern steht an, und Bonny Brain geht zum Verteilen von Geschenken mit ihren Crewmitgliedern als Wookiee verkleidet in ein Waisenhaus.
Aufgabe:
Erzeuge mit
Executors.newCachedThreadPool()
einenExecutorService
, das ist der Thread-Pool.Lege ein String-Array mit Geschenken an.
Bonny Brain läuft im
main
-Thread jedes Geschenk ab und übermittelt es im Abstand von 1 bis 2 Sekunden an ein Crewmitglied, also einen Thread im Thread-Pool.Die Crewmitglieder sind Threads aus dem Thread-Pool. Sie führen die Befehle von Bonny Brain aus, ein Geschenk zu verteilen. Dafür brauchen sie zwischen 1 und 4 Sekunden.
Der Ablauf ist dann wie folgt: Die Geschenkverteilung ist durch ein
Runnable
implementiert, die eigentliche Aktion. Ein freier Thread aus dem Thread-Pool (das Crewmitglied) wird ausgewählt und führt dasRunnable
aus. DasRunnable
benötigt eine Möglichkeit, das Geschenk von Bonny Brain entgegenzunehmen.
1.3.2. Letzte Modifikation von Webseiten ermitteln ⭐⭐
Folgende Klasse implementiert eine Methode, die einen Zeitstempel zurückgibt, in der eine Webseite zum letzten Mal verändert wurde (die Daten sind unter Umständen nicht verfügbar, dann steht die Zeit auf dem 1.1.1970). Der Server sollte in Zonenzeit UTC ± 0 senden.
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class WebChecker {
public static void main(String[] args) throws IOException {
ZonedDateTime urlLastModified = getLastModified(new URL("http://www.tutego.de/index.html"));
System.out.println(urlLastModified);
ZonedDateTime urlLastModified2 = getLastModified(new URL("https://en.wikipedia.org/wiki/Main_Page"));
System.out.println(urlLastModified2);
}
private static ZonedDateTime getLastModified(URL url) {
try {
HttpURLConnection con = (HttpURLConnection) url.openConnection();
long dateTime = con.getLastModified();
con.disconnect();
return ZonedDateTime.ofInstant( Instant.ofEpochMilli( dateTime ), ZoneId.of( "UTC" ) );
} catch ( IOException e ) {
throw new IllegalStateException(e);
}
}
}
Aufgabe:
Lege ein neues Record
WebResourceLastModifiedCallable
mit einer Record-KomponenteURL url
an.Lass
WebResourceLastModifiedCallable
die SchnittstelleCallable<ZonedDateTime>
implementieren. Setze die Implementierung vongetLastModified(URL)
aus dem Beispiel in diecall()
-Methode. Musscall()
die geprüfte Ausnahme selbst auffangen?Baue
WebResourceLastModifiedCallable
Objekte auf, und lasse sie vom Thread-Pool ausführen.Lass das
Callable
einmal ohne Zeitbeschränkung ausführen.Gib dem
Callable
nur eine Mikrosekunde zum Ausführen; was ist das Ergebnis?
Optional: Rechne die Zeit um, seit wie viel Minuten relativ zur aktuellen Zeit sich die Webseite verändert hat.
1.4. Kritische Abschnitte schützen
Laufen mehrere Programmteile nebenläufig, so kann es passieren, dass sie auf gemeinsame Ressourcen oder Speicherbereiche zugreifen. Solche Zugriffe müssen synchronisiert werden, sodass ein Thread die Arbeit beenden kann, bis ein anderer Thread auf diese Ressource geht. Werden die Zugriffe auf gemeinsame Ressourcen nicht koordiniert, kommt es zu fehlerhaften Zuständen.
Programme müssen kritische Abschnitte schützen, sodass sich in einem Abschnitt nur ein anderer Thread befinden darf. Java bietet zwei Mechanismen:
das Schlüsselwort
synchronized
Lock
-Objekte
synchronized
ist ein praktisches Schlüsselwort, allerdings in der Leistungsfähigkeit beschränkt. Die Java Concurrency Utilities bieten leistungsfähigere Datentypen. Zum »Absperren« von exklusiv ausgeführten Programmteilen existieren die Schnittstelle java.util.concurrent.locks.Lock
und diverse Implementierungen, wie ReentrantLock
, ReentrantReadWriteLock.ReadLock
, ReentrantReadWriteLock.WriteLock
.
Lock
und ReentrantLock
1.4.1. Erinnerungen ins Poesiealbum schreiben ⭐
Nachdem ein Frachtschiff mit wertvollem Nepenthe erfolgreich den »Eigentümer« gewechselt hat, schreiben die Freibeuter ihre Erinnerungen in ein Poesiealbum, das Captain CiaoCiao später mit Stickern verziert.
Gegeben ist folgender Programmcode in der main(…)
-Methode einer Klasse:
class FriendshipBook {
private final StringBuilder text = new StringBuilder();
public void appendChar( char character ) {
text.append( character );
}
public void appendDivider() {
text.append(
"\n_,.-'~'-.,__,.-'~'-.,__,.-'~'-.,__,.-'~'-.,__,.-'~'-.,_\n" );
}
@Override public String toString() {
return text.toString();
}
}
class Autor implements Runnable {
private final String text;
private final FriendshipBook book;
public Autor( String text, FriendshipBook book ) {
this.text = text;
this.book = book;
}
@Override public void run() {
for ( int i = 0; i < text.length(); i++ ) {
book.appendChar( text.charAt( i ) );
try { Thread.sleep( 1 ); }
catch ( InterruptedException e ) { /* Ignore */ }
}
book.appendDivider();
}
}
FriendshipBook book = new FriendshipBook();
String q1 = "The flowers need sunshine and " +
"I need Captain CiaoCiao to be happy";
new Thread( new Author( q1, book ) ).start();
String q2 = "When you laugh, they all laugh. " +
"When you cry, you cry alone.";
new Thread( new Author( q2, book ) ).start();
TimeUnit.SECONDS.sleep( 1 );
System.out.println( book );
Aufgabe:
Überlege vor der Ausführung des Programms, welches Ergebnis zu erwarten ist.
Setze den Code in eine eigene Klasse und
main(…)
-Methode, und prüfe die Annahme nach.Die Schwachstelle im Code ist der ungezügelte Zugriff auf das
FriendshipBook
. Verbessere mit einemLock
-Objekt das Programm, damit dasFriendshipBook
nur von einem Piraten zu einer Zeit beschrieben werden kann.
1.5. Thread-Kooperation und Synchronisationshelfer
Es ist wichtig, Programmcode zu synchronisieren, damit nicht zwei Threads sich gegenseitig ihre Daten überschreiben. Wir haben gesehen, dass das mit Lock
-Objekten funktioniert. Lock
-Objekte sperren aber lediglich einen kritischen Bereich, und die Laufzeitumgebung wird automatisch einen Thread warten lassen, wenn ein kritischer Bereich gesperrt ist. Eine Erweiterung davon ist, dass ein Thread — oder mehrere Threads — nicht nur auf den Einlass in einen kritischen Abschnitt wartet, sondern über Signale informiert wird, dass er etwas zu tun hat. Java bietet unterschiedliche Synchronisationshelfer, die einen internen Zustand haben und andere Threads veranlassen, zu warten oder bei gewissen Bedingungen loszulaufen.
Semaphore
: Während sich bei einemLock
nur ein einziger Thread in einem kritischen Abschnitt befindet, dürften sich bei einerSemaphore
eine benutzerdefinierte Anzahl von Threads in einem Block aufhalten. Auch die Methodennamen sind ein wenig anders:Lock
deklariert die Methodelock()
undSemaphore
die Methodeacquire()
. Ist beiacquire()
die maximale Anzahl erreicht, muss ein Thread wie bei einemLock
auf Zugriff warten. Eine Semaphore mit einer maximalen Anzahl von 1 ist wie einLock
.Condition
: Mit einerCondition
kann ein Thread sich warten legen und von einem anderen Thread wieder aufgeweckt werden. Mithilfe vonCondition
-Objekten lassen sich Konsumenten-Produzenten-Verhältnisse programmieren, allerdings gibt es in der Praxis kaum Notwendigkeit für diesen Datentyp, denn es gibt darauf aufbauende Java-Typen, die oftmals einfacher und flexibler sind.Condition
ist eine Schnittstelle, und dasLock
-Objekt bietet Fabrikmethoden, dieCondition
-Instanzen liefern.CountDownLatch
: Objekte vom TypCountDownLatch
werden mit einer Ganzzahl initialisiert, und verschiedene Threads zählen diesenCountDownLatch
herunter, was sie in einen Wartezustand bringt. Wenn zum Schluss derCountDownLatch
bei 0 angekommen ist, werden alle Threads wieder freigelassen. EinCountDownLatch
ist somit eine Möglichkeit, verschiedene Threads an einem gemeinsamen Punkt zusammenzubringen. Wenn einCountDownLatch
einmal verbraucht ist, lässt er sich nicht wieder zurücksetzen.CyclicBarrier
: Die Klasse ist eine Implementierung einer sogenannten Barriere. Mit einer Barriere können sich mehrere Threads an einem Punkt treffen. Werden z. B. Arbeitsaufträge parallelisiert und müssen sie später wieder zusammengefügt werden, so lässt sich das über eine Barriere realisieren. Nachdem alle Threads an dieser Barriere zusammenkommen sind, laufen sie weiter. Dem Konstruktor vonCyclicBarrier
kann einRunnable
übergeben werden, das zum Zeitpunkt des Zusammentreffens aufgerufen wird. Im Gegensatz zu einemCountDownLatch
lässt sich einCyclicBarrier
zurücksetzen und wiederverwenden.Exchanger
: Produzenten-Konsumenten-Verhältnisse kommen in Programmen häufig vor, und der Produzent übermittelt dem Konsument Daten. Genau für diesen Fall gibt es die KlasseExchanger
. Damit können zwei Threads sich an einem Punkt treffen und Daten austauschen.
1.5.1. Am Bankett mit den Captains teilnehmen — Semaphore ⭐⭐
Bonny Brain und Captain CiaoCiao planen ein Bankett mit vielen Weggefährten. Sie sitzen beide an einem Tisch mit 6 Plätzen und empfangen unterschiedliche Gäste. Die Gäste kommen, bleiben ein wenig, erzählen und essen und verlassen wieder den Tisch.
Aufgabe:
Lege eine
Semaphore
mit genauso vielen Plätzen an, wie es gleichzeitig Gäste an dem Tisch mit den Captains geben kann.Modelliere einen Gast als Record
Guest
, dasRunnable
implementiert. Alle Gäste haben einen Namen.Die Gäste warten auf einen Platz. Es muss nicht »fair« sein, dass also der Gast, der schon am längsten wartet, zwangsläufig als nächster an den Tisch kommt.
Das Programm soll eine Bildschirmausgabe tätigen für einen Gast, der gerne an den Tisch kommen möchte, für einen Gast, der einen Platz bekommen hat, und für einen Gast, der den Tisch verlässt.
1.5.2. Fluchen und beleidigen — Condition ⭐⭐
Piraten duellieren sich heutzutage nicht mehr mit Entersäbeln, sondern mit Flüchen.
Aufgabe:
Starte zwei Threads, die jeweils zwei Piraten repräsentieren; gib den Threads Namen.
Ein zufälliger Pirat startet mit dem Fluchen und bringt einen endlosen Beleidigungswettbewerb in Gang.
Die Flüche sollen zufällig aus einer vorgegebenen Sammlung von Flüchen stammen.
Vor dem eigentlichen Fluch kann ein Pirat eine »Denkpause« von bis zu einer Sekunde benötigen.
1.5.3. Stifte aus dem Malkasten nehmen — Condition ⭐⭐
Im Kindergarten kommen regelmäßig die kleinen Piraten zusammen und malen Bilder. Leider gibt es nur einen Kasten mit 12 Stiften. Wenn ein Kind Stifte aus dem Kasten genommen hat, muss ein anderes Kind immer dann warten, wenn es mehr Stifte möchte, als im Kasten verfügbar sind.
Das Szenario lässt sich gut mit Threads realisieren.
Aufgabe:
Schreibe eine Klasse
Paintbox
für einen Malkasten.Paintbox
soll einen Konstruktor bekommen und die maximale Anzahl der freien Stifte annehmen.In der Klasse
Paintbox
soll es eine MethodeacquirePens(int numberOfPens)
geben, mit der die Kinder eine Anzahl Stifte erfragen können. Die gewünschte Anzahl Stifte kann über der verfügbaren Anzahl liegen, dann soll die Methode so lange blockieren, bis die Anzahl gewünschter Stifte wieder verfügbar ist.Die Klasse
Paintbox
verfügt zusätzlich über die MethodereleasePens(int numberOfPens)
zum Zurücklegen der Stifte. Diese Methode signalisiert, dass wieder Stifte verfügbar sind.Lege eine Klasse
Child
an.Gib der Klasse
Child
einen Konstruktor, sodass jedes Kind einen Namen hat und einen Verweis auf einen Malkasten bekommen kann.Die Klasse
Child
soll die SchnittstelleRunnable
implementieren. Die Methode soll eine Zufallszahl zwischen 1 und 10 bestimmen, die die Anzahl der gewünschten Stifte repräsentiert. Dann erfragt das Kind diese Anzahl Stifte vom Malkasten. Das Kind nutzt die Stifte zwischen 1 und 3 Sekunden lang und legt anschließend alle Stifte wieder in den Malkasten zurück — nicht mehr und auch nicht weniger. Danach wartet das Kind zwischen 1 und 5 Sekunden und beginnt erneut damit eine zufällige Anzahl Stifte zu fordern.
Das Malen kann mit folgenden Kindern gestartet werden:
public static void main( String[] args ) {
Paintbox paintbox = new Paintbox( 12 );
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit( new Child( "Mirjam", paintbox ) );
executor.submit( new Child( "Susanne", paintbox ) );
executor.submit( new Child( "Serena", paintbox ) );
executor.submit( new Child( "Elm", paintbox ) );
}
1.5.4. Schere, Stein, Papier spielen — CyclicBarrier ⭐⭐⭐
Schere, Stein, Papier (auch Schnick, Schnack, Schnuck) ist ein altes Spiel, das schon im 17. Jahrhundert gespielt wurde. Nach einem Startsignal bilden die beiden Spieler mit einer Hand eine Form für Schere, Stein oder Papier. Welcher Spieler gewinnt, bestimmen folgende Regeln:
Schere schneidet das Papier (Schere gewinnt).
Papier wickelt den Stein ein (Papier gewinnt).
Stein macht die Schere stumpf (Stein gewinnt).
Jedes Handzeichen kann also gewinnen oder verlieren.
Wir wollen eine Simulation für das Spiel schreiben und nehmen folgende Aufzählung für Handzeichen als Basis:
enum HandSign {
SCISSORS, ROCK, PAPER;
static HandSign random() {
return values()[ ThreadLocalRandom.current().nextInt( 3 ) ];
}
int beats( HandSign other ) {
return (this == other) ? 0 :
(this == HandSign.ROCK && other == HandSign.SCISSORS
|| this == HandSign.PAPER && other == HandSign.ROCK
|| this == HandSign.SCISSORS && other == HandSign.PAPER) ? +1 : -1;
}
}
Die enum
HandSign
deklariert drei Aufzählungselemente für Schere, Stein, Papier. Die statische Methode random()
liefert ein zufälliges Handzeichen. Die Methode beats(HandSign)
ist ähnlich wie eine Comparator
-Methode: Sie vergleicht das aktuelle Handzeichen mit dem übergebenen Handzeichen und liefert 0
, wenn beide Handzeichen gleichwertig sind, +1
, wenn das eigene Handzeichen höherwertig ist als das übergebene Anzeichen, und -1
sonst.
Aufgabe:
Lasse einen Starter-Thread laufen, der jede Sekunde ein Schnick-Schnack-Schnuck-Spiel anstößt. Für die wiederholte Ausführung lässt sich auf einen
ScheduledExecutorService
zurückgreifen.Ein Spieler ist durch ein
Runnable
repräsentiert, das ein zufälliges Handzeichen wählt und die Wahl mitadd(…)
in eine Datenstruktur vom TypArrayBlockingQueue
setzt.Nachdem ein Spieler ein Handzeichen gewählt hat, soll auf einer vorher aufgebauten
CyclicBarrier
dieawait()
-Methode aufgerufen werden.Der Konstruktor von
CyclicBarrier
soll einRunnable
bekommen, das am Ende des Spiels den Gewinner ermittelt. DasRunnable
nimmt aus derArrayBlockingQueue
die beiden Handzeichen mitpoll()
heraus, vergleicht sie und wertet den Gewinner und Verlierer aus. An der ersten Stelle der Datenstruktur steht Spieler 1, an der zweiten Position Spieler 2.
1.5.5. Den schnellsten Läufer finden — CountDownLatch ⭐⭐
Für den nächsten Überfall benötigt Bonny Brain schnelle Läufer. Dafür richtet sie einen Wettbewerb aus und lässt die besten Läufer antreten. Mit einer Startpistole steht Bonny Brain an der Bahn, und alle warten auf den Startschuss.
Aufgabe:
Lege 10 Threads an, die auf das Signal von Bonny Brain warten. Dann starten die Threads und benötigen zwischen einer frei gewählten Anzahl von 10 und 20 Sekunden für den Lauf. Am Ende sollen die Threads ihre Zeit in eine gemeinsame Datenstruktur schreiben, sodass der Name des Threads (Läufername) mit der Laufzeit vermerkt wird.
Bonny Brain startet die Läufer im
main
-Thread und gibt zum Schluss alle Laufzeiten aufsteigend sortiert mit den Läufernamen aus.
Sollen mehrere Threads an einer Stelle zusammenkommen, lässt sich dafür gut ein
|
Noch mehr Aufgaben findest du im Buch: ›Captain CiaoCiao erobert Java: Das Trainingsbuch für besseres Java. 300 Java-Workshops, Aufgaben und Übungen mit kommentierten Lösungen‹