Parallele Programmierung (3IB)
=> Grundlage: Hettel und Tran (2016, Kap. 5 und Abschnitt 8.1.3)
synchronized
synchronized und einem Warteraum
“Eine Menge von Prozessen befindet sich in einem Deadlock-Zustand, wenn jeder Prozess aus der Menge auf ein Ereignis wartet, das nur ein anderer Prozess aus der Menge auslösen kann.”1
Ein Prozess “verhungert”, wenn er nie Zugriff auf eine Ressource erhält, die von mehreren Prozessen beansprucht wird.
Eine Menge Prozessen befindet sich in einem Livelock-Zustand, wenn mindestens ein Prozess verhungert, weil die Kontrolle zwischen anderen Prozessen hin und her geht, ohne dass verhungernde Prozesse die Kontrolle jemals mehr erlangen können. (Kein Deadlock!)
Problemsituation
Lösung (eine von mehreren möglichen): Prioritätsvererbung
Beispiel: Pathfinder (NASA Mars-Fahrzeug, 1996)1
Die Bedeutung im Java-Umfeld ist gering oder zumindest unklar, da kein spezifisches Scheduling Verfahren für JVM vorgegeben ist.
synchronizedsynchronized)synchronizedsynchronized in privater Implementierung einer Methode in externer Klasse - z.B. Library)static Object l1 = new Object();
static Object l2 = new Object();
static void m1() {
synchronized (l1) {
synchronized (l2) {
/* ... */
}
}
}
static void m2() {
synchronized (l2) {
synchronized (l1) {
/* ... */
}
}
}
public static void main(String... args) {
var t1 = new Thread(Deadlock::m1); t1.start();
var t2 = new Thread(Deadlock::m2); t2.start();
}pp.03.01-SynchPhilosopherDer Lebenszyklus eines/r Philosoph:in ist:
Greifen von Esstäbchen ist kritischer Abschnitt

Wenn alle Threads auf die Locks in derselben numerischen Reihenfolge zugreifen (in den synchronized-Block eintreten), dann kommt es nicht zu einem Deadlock (“global ordering rule”).
Die meisten Philosophen folgen diesem Schema: left und right sind Chopstick-Objekte. Philosoph \(i\) und Philosoph \(i+1\) teilen ein Chopstick-Objekt. Für Philosoph \(i\) ist das das rechte, auf das als Zweites zugegriffen wird, für Philosoph \(i+1\) ist das das linke, auf das als Erstes zugegriffen wird.
Nur im Fall des letzten bzw. 0. Philosophs wird diese Reihenfolge umgekehrt, daher kann es zum Deadlock kommen.
“To my mind, what makes multithreaded programming difficult is not that writing it is hard, but that testing it is hard. It’s not the pitfalls that you can fall into; it’s the fact that you don’t necessarily know whether you’ve fallen into one of them.”1
synchronized und einem Warteraumwait + notify/notifyAll)Manchmal ist es erforderlich Threads über Ereignisse zu steuern.
Dazu dient ein der Monitor eines “Vermittlerobjekts”. Es muss lediglich von Object erben. Die beteiligten Threads kommunizieren über den Monitor dieses Vermittlers miteinander.
Ein Thread muss im Besitz des Monitors des Vermittlers sein, um die Methoden wait(), notify() und notifyAll() aufrufen zu dürfen, sonst wird eine IllegalMonitorStateException geworfen.
Diese Methoden können somit nur innerhalb von synchronized-Blöcken bzw. -Methoden verwendet werden.
Alle wartenden Threads sind in der condition queue des Locks, auch condition variable oder Warteraum genannt.
obj.wait():
t1 prüft das Vorhandensein einer Wartebedingung (ist in der Regel kritischer Abschnitt, daher in synchronized-Block).t1 muss spätestens jetzt den Monitor (“Schloss”) des vermittelnden Lock-Objekts obj belegt haben.t1 in die Warteliste des vermittelnden Lock-Objekts obj ein.t1 gibt den Monitor von obj wieder frei.t1 wechselt in den WAITING-Zustand und bleibt so lange darin, bis ihn ein anderer Thread (im Folgenden t2) durch obj.notify() oder obj.notifyAll() weckt oder er ein t1.interrupt() erhält.obj.notifyAll():
t2 muss den Monitor des vermittelnden Lock-Objekts obj belegt haben.obj warten (neuer Zustand RUNNABLE).obj wieder, bevor er mit den Anweisungen nach wait() fortfährt. Daher kann nur einer der wartenden Threads fortfahren, obwohl alle aufgeweckt wurden.obj.notify():
t2 muss den Monitor des vermittelnden Lock-Objekts obj belegt haben.obj wartet, und weckt ihn auf (neuer Zustand RUNNABLE).obj wieder, bevor er mit den Anweisungen nach wait() fortfährt.wait + notify/notifyAll)while: Falls in der Zwischenzeit die Bedingung wieder eingetreten ist oder der Thread “fehlerhaft” geweckt wurde; if statt while wäre hier ein Fehler!notifyAll: Falls mehrere Threads auf unterschiedliche Bedingungen an obj warten, muss zum Signalisieren immer notifyAll() benutzt werden!Start:
items.length |
8 |
Kapazität von items |
count |
0 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
0 |
Index nächstes put() |
count == 0)take()-Aufrufe warten.
put(42):
items.length |
8 |
Kapazität von items |
count |
1 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
1 |
Index nächstes put() |
42 ist an Index 0 von itemsitems
put(99):
items.length |
8 |
Kapazität von items |
count |
2 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
2 |
Index nächstes put() |
99 wurde als nächstes gespeichert
put(80):
items.length |
8 |
Kapazität von items |
count |
3 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
3 |
Index nächstes put() |
80 wurde als nächstes gespeichert
put(12):
items.length |
8 |
Kapazität von items |
count |
4 |
aktuelle Anzahl Elemente |
takeptr |
0 |
Index nächstes take() |
putptr |
4 |
Index nächstes put() |
12 wurde als nächstes gespeichert
take() \(\to\) 42:
items.length |
8 |
Kapazität von items |
count |
3 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
4 |
Index nächstes put() |

put(20):
items.length |
8 |
Kapazität von items |
count |
4 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
5 |
Index nächstes put() |
20 wird gespeichert und putptr wird weiterbewegttakeptr bleibt unverändert
put(14):
items.length |
8 |
Kapazität von items |
count |
5 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
6 |
Index nächstes put() |
14 wird gespeichert und putptr wird weiterbewegttakeptr bleibt unverändert
put(44):
items.length |
8 |
Kapazität von items |
count |
6 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
7 |
Index nächstes put() |
44 wird gespeichert und putptr wird weiterbewegttakeptr bleibt unverändert
put(75):
items.length |
8 |
Kapazität von items |
count |
7 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
0 |
Index nächstes put() |
75 wird gespeichert und putptr wird weiterbewegttakeptr bleibt unverändert
put(13):
items.length |
8 |
Kapazität von items |
count |
8 |
aktuelle Anzahl Elemente |
takeptr |
1 |
Index nächstes take() |
putptr |
1 |
Index nächstes put() |
items.length == count)put()-Operationen müssen warten
take() \(\to\) 99:
items.length |
8 |
Kapazität von items |
count |
7 |
aktuelle Anzahl Elemente |
takeptr |
2 |
Index nächstes take() |
putptr |
1 |
Index nächstes put() |
take() entfernt Element an Index 1items.length > count)
put(22):
items.length |
8 |
Kapazität von items |
count |
8 |
aktuelle Anzahl Elemente |
takeptr |
2 |
Index nächstes take() |
putptr |
2 |
Index nächstes put() |
count == items.lengthput() muss wartencount == 0take() muss warten
wait/notifyAll für Ringpuffer”pp.03.02-BoundedQueueWaitNotify
synchronized vorhanden, auf der wait()/notify() bzw. notifyAll() ausgeführt werden kann.notifyAll() aufgeweckt werden, kann es sein, dass Verarbeitungskapazizät vergeudet wird. Mit Condition können mehrere getrennte “Warteräume” etabliert werden.Condition aus dem Package java.util.concurrent.locks) in einem “Monitor” zu verwenden.synchronized muss dann aber das Interface Lock aus dem Package java.util.concurrent.locks benutzt werden, um den Monitor zu begrenzen.Lock)Condition)import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (/* «Wartebedingung» */) {
condition.await();
}
/* «Benutzung geteilter Ressourcen» */
} finally {
lock.unlock();
}Condition)Wenn die wartenden Threads nun “bedingungsgenau” in getrennten Warteräumen (für mehrere Bedingungsvariablen) verweilen, kann signal() statt signalAll() zum Aufwecken benutzt werden, da sichergestellt ist, dass ein zufällig ausgewählter aufgeweckter Thread eine Situation vorfindet, in der die Bedingung, auf die gewartet wurde, erfüllt ist.
Würden mehrere Threads auf unterschiedliche inhaltliche Bedingungen in demselben Warteraum warten, könnte es sein, dass ein Thread geweckt wird, der auf eine Bedingung wartet, die noch nicht eingetreten ist – er ruft daher await() für sich auf. Damit wären alle Threads dann im Zustand WAITING und es gäbe keine Möglichkeit, dass einer andere wecken könnte.
Condition aus dem Package java.util.concurrent.locksimport java.util.Date;
import java.util.concurrent.TimeUnit;
public interface Condition {
public void await() // analog wait()
throws InterruptedException;
public void awaitUninterruptibly();
public long awaitNanos(long nanos)
throws InterruptedException;
public boolean await(long time, TimeUnit unit)
throws InterruptedException;
public boolean awaitUntil(Date deadline)
throws InterruptedException;
public void signal(); // analog notify()
public void signalAll(); // analog notifyAll()
}await/signal für Ringpuffer”