Musterlösungen
Laboraufgabe “Dinierende Philosoph:innen”
Das Problem der Verklemmung resultiert in der ursprünglichen Lösung aus der Verletzung der “global ordering rule”: Die Philosophen sind an einem runden Tisch angeordnet, der über Arrays repräsentiert wird.
chopsticks
ist ein Array, in demChopstick
-Objekte gespeichert werden.philosophers
ist ein Array, in dem diePhilosopher
-Objekte (erben vonThread
) gespeichert werden.
Jeder Philosoph mit Index \(i\) im Array philosophers
greift folgendermaßen auf die Essstäbchen zu:
- links:
chopsticks[i]
- rechts:
chopsticks[i+1]
Da es aber nur so viele Essstäbchen wie Philosophen gibt, würde beim letzten Philosophen der Zugriff auf das rechte Essstäbchen fehlschlagen:
bzw.
Um die Kreiseigenschaft mit diesen Arrays abzubilden, wird für den Zugriff auf das rechte Essstäbchen modulo der Anzahl der Philosophen gerechnet:
- links:
chopsticks[i]
- rechts:
chopsticks[(i + 1) % PHILOSOPHER_NUM]
Dadurch dreht sich beim Zugriff des “letzten Philosophen”, also dem mit Index PHILOSOPHER_NUM]
die Reihenfolge des Zugriffs um: Zuerst wird auf das Essstäbchen mit Index PHILOSOPHER_NUM-1
zugegriffen, danach auf das Stäbchen mit Index 0
. Die richtige Reihenfolge müsste aber wie bei den anderen Zugriffen der numerischen Reichenfolge folgen. Um dieses Problem zu beheben und sicher Verklemmungen zu vermeiden, muss daher beim letzten Philosophen also zuerst auf das rechte Essstäbchen und dann auf das linke zugegriffen werden.
Um dies zu erreichen, könnte die Methode eat()
von Philosopher
umgebaut werden. Da diese Methode aber von allen Philosophen verwendet wird (also auch von denen, bei denen die “reguläre” Reienfolge verwendet werden muss), würde diese Lösung recht unübersichtlich ausfallen.
Einfacher ist es, diese Methode unangetastet zu lassen und beim letzten Philosophen im Array nur umzudrehen, welches Stäbchen links und welches rechts liegt. Dazu muss die main
-Methode von PhilosopherExperiment
modifiziert werden:
public static void main(String... args) throws InterruptedException {
var chopsticks = new Chopstick[PHILOSOPHER_NUM];
var philosophers = new Philosopher[PHILOSOPHER_NUM];
for (int i = 0; i < PHILOSOPHER_NUM; i++) {
chopsticks[i] = new Chopstick();
}
// first n-1 philosophers
for (var i = 0; i < (PHILOSOPHER_NUM - 1); i++) {
philosophers[i] = new Philosopher(i, chopsticks[i],
chopsticks[(i + 1)]);
}
// nth philosophers: initialize with right+left instead of left+right
philosophers[PHILOSOPHER_NUM - 1] = new Philosopher(PHILOSOPHER_NUM - 1,
chopsticks[0], chopsticks[PHILOSOPHER_NUM - 1]);
for (var i = 0; i < PHILOSOPHER_NUM; i++) {
philosophers[i].start();
}
Thread.sleep(EXP_DURATION_MS);
for (var i = 0; i < PHILOSOPHER_NUM; i++) {
philosophers[i].stopPhilosopher();
}
}
Laboraufgabe “wait
/notifyAll
für Ringpuffer”
Analyse des Quellcode
Analysieren Sie den Quellcode.
- Es handelt sich um einen Ringspeicher. Das Warten solange der Speicher leer ist (
take
) bzw. solange der Speicher voll ist (put
) ist aber noch nicht umgesetzt.
Laufverhalten
Lassen Sie das Hauptprogramm mehrfach laufen und beobachten Sie das Verhalten. Entspricht dies der Spezifikation?
- Dementsprechend kann es in Probeläufen dazu kommen, dass Elemente des Rinpuffers überschrieben werden, denn es wird aufgrund des Testtreibers (
main
-Methode) seltener entnommen als hinzugefügt. Das zeigt sich dann daran, dass eigentlich vorhandene Elemente vontake
nicht mehr entnommen werden können, da sie überschrieben wurden. Das zeigt die folgende Beispielausgabe:
Producer-1: put=1
Producer-2: put=1
Producer-2: put=2
Producer-2: put=3
Producer-2: put=4
Producer-2: put=5
Producer-2: put=6
Producer-2: put=7
Ab hier eigentlich voll, aber es geht weiter:
Producer-2: put=8
Dadurch wurde das erste Element “1” mit “8” überschrieben. Es wird auch entsprechend hier gelesen:
Consumer-1: taken=8
Modifikation von put
und take
, so dass gewartet wird, falls der Puffer voll ist
Modifizieren Sie die Methode put
so, dass gewartet wird, falls der Puffer voll ist (this.count == this.items.lentgh
). Dazu muss take
alle wartenden Threads aufwecken, wenn ein Element entfernt wurde, weil im Puffer danach wieder etwas Platz ist.
solange der Speicher voll ist, warten:
Da
this.count
bei jedem Hinzufügen um1
erhöht und bei jedem Lesen um1
verringert wird, kann man an einem Vergleich der Instanzvariablenthis.count
mit der Größe des Container-Arraysthis.items
(hier 8) feststellen, ob noch Platz im Speicher ist. In dem Fall muss der Aufrufer durchwait()
blockiert werden.
Durch den Aufruf von wait()
an this
, also an der jeweiligen Instanz von BoundedBuffer
wird der Warteraum von this
verwendet. In diesem Warteraum werden die durch den Aufruf von put
an einem bereits vollen Ringpuffer blockierten Threads warten.
Gibt es mehrere Instanzen von BoundedBuffer
beeinflussen sie sich bzw. die Threads, die sie nutzen, nicht gegenseitig.
nun ist etwas Platz im Speicher: Alle wartenden Threads benachrichtigen:
Threads, die möglicherweise durch Aufruf von
wait()
anthis
blockiert sind, müssen nun aufgeweckt werden. Dies erfolgt durch den Aufruf vonnotifyAll()
anthis
. In diesem Beispiel würde es auch funktionieren, einen beliebigen wartenden Thread mitnotify()
zu wecken, da es nicht passieren kann, dass anthis
Threads sowohl auf die eine Bedingung (nicht mehr voll), als auch auf die anderen Bedingung (nicht mehr leer) warten. Es würde also mitnotify()
immer ein auf die richtige Bedingung wartender Thread geweckt werden.synchronized
:Der Aufruf von
wait()
undnotify()
/notifyAll()
darf nur auf einem Bereich gegenseitigen Ausschluss geschehen. Deshalb werden die beiden Methodenput
undtake
mitsynchronized
geschützt.
Modifikation von take
und put
, so dass gewartet wird, falls der Puffer leer ist
Modifizieren Sie die Methode take
so, dass gewartet wird, falls der Puffer leer ist (items.count==0
). Dazu muss put
alle wartenden Threads aufwecken, wenn ein Element hinzugefügt wurde, weil im Puffer danach wieder etwas zu holen ist.
solange der Speicher leer ist, warten:
Da
count
beginnend mit 0 bei jedem Hinzufügen um 1 erhöht und bei jedem Lesen um 1 verringert wird, kann man an der Instanzvariablencount
feststellen, ob der Speicher leer ist. In dem Fall muss der Aufrufer durchwait()
blockiert werden.
- nun ist etwas neues im Speicher: Alle wartenden Threads benachrichtigen:
Threads, die möglicherweise durch Aufruf von wait()
an this
blockiert sind, müssen nun aufgeweckt werden. Dies erfolgt durch den Aufruf von notifyAll()
an this
. In diesem Beispiel würde es auch funktionieren, einen beliebigen wartenden Thread mit notify()
zu wecken, da es nicht passieren kann, dass an this
Threads sowohl auf die eine Bedingung (nicht mehr voll), als auch auf die anderen Bedingung (nicht mehr leer) warten. Es würde also mit notify()
immer ein auf die richtige Bedingung wartender Thread geweckt werden.
Laboraufgabe “await
/signal
für Ringpuffer”
Modifizieren Sie die Methoden put
und take
so, dass zwei getrennte Bedingungsvariablen verwendet werden.
- Statt
synchronized
wird hierReentrantLock
für den gegenseitigen Ausschluss verwendet, da mitnewCondition
Bedingungsvariablen zumLock
erzeugt werden können. Es eergeben sich also die folgenden zusätzlichen Instanzvariablen mit Initialisierung:
final Lock lock = new ReentrantLock();
final Condition notFull = this.lock.newCondition();
final Condition notEmpty = this.lock.newCondition();
- Die beiden Bedingungsvariablen sollen die beiden Situationen repräsentieren, in denen gewartet werden muss: “Speicher schon voll”, “Speicher noch leer”.
- Dementsprechend müssen die kritischen Regionen (hier die kompletten Rümpfe der Methoden
put
undtake
) mit gegenseitigem Ausschluss geschützt werden:
- Auf das Eintreten der jeweiligen Bedingung warten:
An den beiden Bedingungen ändert sich nichts. Lediglich das Warten (Blockieren des Aufrufer-Threads) erfolgt nun durch Aufruf vonawait()
an jeweils einer der beiden Bedingungsvariablen.
bzw.
- Das Lösen der Blockierung geschieht hier durch Aufruf von
signal()
(signalAll()
ist im Allgemeinen nicht erforderlich, da durch Bedingungsvariablen immer “bedingungsgenau” gewartet werden kann). Der Aufruf muss “überkreuz” erfolgen. D.h. in der Methode, in der annotFull
await()
aufgerufen wurde, musssignal()
annotEmpty
aufgerufen werden und umgekehrt.