6 Lock-Objekte und Semaphore
6.1 Nachteile von “synchronized”
- Durch
synchronizedblockierter Thread kann nicht mitinterrupt()unterbrochen werden.interrupt()wirkt sich erst nach der Blockierung aus. - kein Timeout
- keine Regel für Reihenfolge der Zuteilung an mehrere wartende Tasks
- nur Blockstruktur (Eintritt und Austritt in unterschiedlichen Methoden nicht möglich)
6.2 ReentrantLock
6.2.1 Lock-Implementierungen und ihre Beziehungen
6.2.2 Lock-Interface
- ein wegen
lockInterruptably()blockierter Thread kann durchinterrupt()unterbrochen werden (\(\to\)InterruptedException) tryLock()liefertfalse, falls `Lock`-Instanz schon belegt ist
6.2.3 Nachteile von synchronized
- Durch
synchronizedblockierter Thread kann nicht mitinterrupt()unterbrochen werden.interrupt()wirkt sich erst nach der Blockierung aus. - kein Timeout
- keine Regel für Reihenfolge der Zuteilung an mehrere wartende Tasks
- nur Blockstruktur (Eintritt und Austritt in unterschiedlichen Methoden nicht möglich)
6.2.3.1 Vorteile von Lock-Implementierungen
- Unterbrechbarkeit
- Timeout
- Fairness
- “Non-Blockstruktur”
6.2.4 Benutzungsmuster von Lock-Interface und -Implementierungen
6.2.4.1 unlock() in finally-Block
- Durch
try…finallywird sichergestellt, dass derLockauf jeden Fall am Ende gelöst wird. lock()undunlock()können von unterschiedlichen Methoden aus aufgerufen werden.
6.2.4.2 Fairness und Acquisition Order
fairness-Parameter gibt an, ob als nächstes der am längsten wartende Thread entblockiert wird oder nicht.
6.2.4.2.1 Fairness Policy (optional)
- Non-fair mode (default)
Reihenfolge, in der blockierte Threads entblockiert werden ist unbestimmt und kann nicht beeinflusst werden. Insbesondere ist es nicht unbedingt so, dass der Thread als nächstes den Zuschlag bekkommt, der am längsten warten musste. - Fair mode
Reihenfolge der Entblockierung entspricht der “Ankunftsreihenfolge” am Lock. Der Thread, der am längsten am Lock wartet, wird als nächstes entblockiert.
6.2.4.3 tryLock()
- mit
tryLock()prüfen, obLockbelegt und schließen oder entwas anderes tun
6.3 ReadWriteLock
6.3.1 Lock-Implementierungen und ihre Beziehungen
6.3.2 ReadWriteLock
- warum sollten nicht mehrere Threads gleichzeitig lesend auf eine Variable zugreifen dürfen, wenn ein gleichzeitiges Schreiben ausgeschlossen ist?
- Eine Lock-Variante, die
ReadLockundWriteLockbesitztReadLock: mehrere dürfen in den kritischen Abschnitt, wenn der dazugehörendeWriteLocknicht geschlossen ist (“nicht-exklusiver” Lock).WriteLock: nur einer darf in den kritischen Abschnitt und auch nur, wenn derReadLocknicht gerade benutzt wird (“exklusiver” Lock).
- Die Vergabe der Locks erfolgt in der Reihenfolge, wie sie angefordert wurden.
Besitzt ein Thread eine Lesesperre und versucht ein weiterer Thread, eine Schreibsperre zu erhalten, muss dieser warten. Die Frage ist nun, was passiert, wenn jetzt ein Thread kommt, der eine Lesesperre erwerben möchte. Aus Gründen des Durchsatzes bekommt er sie sofort. Eine solche Vergabestrategie kann aber dazu führen, dass ein Thread, der eine Schreibsperre anfordert, unter Umständen nie zum Zuge kommt (\(\to\) lock starvation).
rLockundwLockwerden benutzt wie die bisherigenLock-Implementierungen- Sie verhalten sich aber anders (s.o.) und sind “miteinander verschränkt”.
6.3.3 Beispiel: Verschränkung Read/Write Lock
- Zu jeder Instanz einer Implementierung von
ReadWriteLockwird je ein Read-Lock und ein Write-Lock erzeugt. - Ihr Typ implementiert das
Lock-Interface. - Nutzer eines Read- oder Write-Locks müssen eine Referenz auf das
ReadriteLock-Objekt bekommen. Das ist im Sequenzdiagramm nur durchnew(lock)angedeutet. Natürlich erwartet der Konstruktor vonThreadkeinLock-Objekt. - Read-Lock und Write-Lock des selben
ReadWriteLocksind miteinander “verschränkt” und wirken auf den sie erzeugendenReadWriteLockzurück. Im Sequenzdiagramm ist das nur durch “namenlose Nachrichten” angedeutet.
Im folgenden Sequenzdiagramm sind die Rückwirkungen von Aufrufen an
rlundwlauflocknicht dargestellt.t1schließt zuerst den Read-Lock
\(\to\)lock: gelbt2darf am Read-Lock teilnehmen und wird nicht blockiert.t3möchte den Write-Lock und wird blockiert.t3wird erst entblockiert, wenn sowohlt1als aucht2den Read-Lock wieder freigegeben haben. Der Write-Lock ist nun vont3gesperrt \(\to\)lock: blaut1möchte den Read-Lock und wird blockiert. Erst wennt3den Write-Lock freigibt, bekommtt1den Read-Lock
\(\to\)lock: gelbt1gibt am Ende den Read-Lock wieder frei.Read-Lock sperrt Lesen “non-exklusiv”
Im folgenden Sequenzdiagramm sind die Rückwirkungen von Aufrufen an
rlundwlauflocknicht dargestellt.t1schließt zuerst den Read-Lock
\(\to\)lock: gelbt3möchte den Write-Lock und wird blockiert.t2darf nun wg. offener Lock-Anforderung vont3nicht mehr am Read-Lock teilnehmen und wird blockiert.t3wird wg. der Reihenfolge der Anforderung als nächstes entblockiert, wennt1freigibt. Der Write-Lock ist nun vont3gesperrt \(\to\)lock: blaut2wird erst entblockiert, wennt3den Lock freigibt. \(\to\)lock: gelbt2gibt am Ende den Read-Lock wieder frei.Reihenfolge der
lock()ist bestimmend
6.4 StampedLock
6.4.1 Lock-Implementierungen und ihre Beziehungen
- Falls die zu schützenden kritischen Abschnitte kurz sind, ist der erforderliche Verwaltungsaufwand für einen
Lockrelativ hoch.synchronizedkann von der Laufzeit her schneller sein. StampedLocknützlich bei großem Leseanteil
Exkurs: StampedLock ist in Java 8 eingeführt worden, während Lock, ReentrantLock etc. schon seit Java 7 dabei sind.
6.4.2 StampedLock Modi
Ein StampedLock besteht aus Stamp und Modus
- Modus Writing (“exklusiver” Lock):
writeLock()blockiert für exklusiven Schreibzugriff; liefert einelongID (“stamp”), die fürunlockWrite(long)benutzt werden kann.
keinereadLocksundtryOptimisticReadmöglich. - Modus Reading (“nicht-exklusiver” Lock):
readLock()wartet auf nicht exklusiven-Zugriff; liefert einelongID (“stamp”), die fürunlockRead(long)benutzt werden kann. - Modus Optimistic Reading (kein Lock, aber post-hoc Kollisionserkennung möglich):
tryOptimisticRead()liefert nur dann einelongID (“stamp”), falls der Lock gerade nicht im Modus “Writing” ist.validate(long)lieferttrue, falls der Lock nicht zum Schreiben gesperrt wurde, seit die ID vergeben wurde.
6.4.3 ReadLock beim StampedLock
6.4.4 WriteLock beim StampedLock
alternativ statt lock.unlockRead(stamp) und lock.unlockWrite(stamp) \(\to\) lock.unlock(stamp)
6.4.5 Beispiel für die Modi “exklusiver”/“nicht-exklusiver” Lock
public static void main(String... args) throws InterruptedException {
var lock = new StampedLock();
var t1 = new Thread(() -> {
System.out.println("t1: trying readLock");
var stamp1 = lock.readLock();
System.out.printf("t1: acquired readLock (stamp1 = %d)\n", stamp1);
// ... hier "non-exklusiv" ("ReadLock acquired")
sleep(5);
lock.unlock(stamp1);
System.out.printf("t1: released readLock (stamp1 = %d)\n", stamp1);
sleep(1);
System.out.println("t1: trying readLock");
var stamp4 = lock.readLock();
System.out.printf("t1: acquired readLock (stamp4 = %d)\n", stamp4);
// ... hier "non-exklusiv" ("ReadLock acquired")
lock.unlock(stamp4);
System.out.printf("t1: released readLock (stamp4 = %d)\n", stamp4);
});
t1.start();
var t2 = new Thread(() -> {
sleep(2);
System.out.println("t2: trying readLock");
var stamp2 = lock.readLock();
System.out.printf("t2: acquired readLock (stamp2 = %d)\n", stamp2);
// ... hier "non-exklusiv" ("ReadLock acquired")
sleep(4);
lock.unlock(stamp2);
System.out.printf("t2: released readLock (stamp2 = %d)\n", stamp2);
});
t2.start();
var t3 = new Thread(() -> {
sleep(1);
System.out.println("t3: trying writeLock");
var stamp3 = lock.writeLock();
System.out.printf("t3: acquired writeLock (stamp3 = %d)\n", stamp3);
// ... hier exklusiv ("WriteLock acquired")
sleep(5);
lock.unlock(stamp3);
System.out.printf("t3: released writeLock (stamp3 = %d)\n", stamp3);
});a
}… mit dem folgenden Trace:
t1: trying readLock
t1: acquired readLock (stamp1 = 257)
t3: trying writeLock
t2: trying readLock
t2: acquired readLock (stamp2 = 258)
t1: released readLock (stamp1 = 257)
t2: released readLock (stamp2 = 258)
t3: acquired writeLock (stamp3 = 384)
t1: trying readLock
t3: released writeLock (stamp3 = 384)
t1: acquired readLock (stamp4 = 513)
t1: released readLock (stamp4 = 513)
t1bekommt zuerst den “nicht-exklusiven” Lock (“Read-Lock”) \(\to\)lockist gelbt3möchte den “exklusiven” Lock (“Write-Lock”) und wird blockiert, bis der nicht-exklusive Lock endet \(\to\)t1ist rott2kommt zut1in den “nicht-exklusiven” Lockt1verlässt den “nicht-exklusiven” Lockt3bleibt vorerst weiterhin blockiert, solange bis der letzte Thread den “nicht-exklusiven” Lock verlässtt2tut dies, daraufhin bekommtt3den “exklusiven” Lock \(\to\)lockist blaut1möchte wieder einen “nicht-exklusiven” Lock, wird aber wg.t3blockiert \(\to\) t1 ist rott3gibt den “exklusiven” Lock zurück, erst dann wirdt1entblockiert und erhält den “nicht-exklusiven” Lock \(\to\)lockist gelb
6.4.6 Optimistisches Lesen (Read-Lock) mit StampedLock
- keine Änderungen im kritischen Abschnitt, aber “nicht-exklusiv” lesend wie Read-Lock
- keine “technische” Durchsetzung des Locks, stattdessen:
- Beginn des kritischen Abschnitts beim
StampedLock-Objekt “markieren” (anmelden) - versuchen, den kritischen Abschnitt abzuarbeiten
- am Ende beim
StampedLock-Objekt nachfragen, ob es seit der Anmeldung Write-Locks gegeben hat (Read-Locks sind unkritisch) - falls ja: reagieren mit Roll-Back der Transaktion und erneutem Versuch
- falls nein: kritischer Abschnitt erfolgreich
- Beginn des kritischen Abschnitts beim
- da kein Blockieren: sehr geringer Overhead
- falls das Anwendungsprofil es zulässt (geringe Wahrscheinlichkeit für Verletzung des “nicht-exklusiven” Locks), die perfomanteste Lösung für konkurrierenden Zugriff:
- bei Zugriff auf einzelne Variablen: Atomics-Wrapper
- bei komplexeren kritischen Abschnitten: optimistische Nutzung von
StampedLock
var lock = new StampedLock();
var stamp = lock.tryOptimisticRead();
//... kritischer Abschnitt ("nicht exklusiv")
if (!lock.validate(stamp)) {
// nicht erfolgreich => das bisherige ist möglicherweise
// inkonsistent und muss zurückgerollt werden; eine mögliche
// Strategie: nochmal pessimistisch "non-excl." gelockt probieren:
stamp = lock.readLock();
try {
//... kritischer Abschnitt (ReadLock)
} finally {
lock.unlockRead(stamp);
}
}andere Muster denkbar (z.B. wie bei Atomics: weiter optimistisch versuchen, bis erfolgreich \(\to\) s. nächstes Beispiel)
6.4.7 Beispiel für optimistisches Lesen (“nicht-exklusiv”)
public static void main(String... args) throws InterruptedException {
var lock = new StampedLock();
var t1 = new Thread(() -> {
long stamp1;
do {
System.out.println("t1: optimisitically trying...");
// ... ggf. Rolback des vorigen optimist. Versuchs
stamp1 = lock.tryOptimisticRead();
System.out.printf("t1: begin opt. (stamp1 = %d)\n", stamp1);
// ... hier optimistischer Versuch ("non-exklusiv")
sleep(5);
System.out.printf("t1: validate opt. (stamp1 = %d)\n", stamp1);
} while (!lock.validate(stamp1));
System.out.printf("t1: was valid (stamp1 = %d)\n", stamp1);
});
t1.start();
var t2 = new Thread(() -> {
sleep(7);
System.out.println("t2: trying readLock");
var stamp2 = lock.readLock();
System.out.printf("t2: acquired readLock (stamp2 = %d)\n", stamp2);
// ... hier "non-exklusiv" ("ReadLock acquired")
sleep(4);
lock.unlock(stamp2);
System.out.printf("t2: released readLock (stamp2 = %d)\n", stamp2);
});
t2.start();
var t3 = new Thread(() -> {
sleep(1);
System.out.println("t3: trying writeLock");
var stamp3 = lock.writeLock();
System.out.printf("t3: acquired writeLock (stamp3 = %d)\n", stamp3);
// ... hier exklusiv ("WriteLock acquired")
sleep(5);
lock.unlock(stamp3);
System.out.printf("t3: released writeLock (stamp3 = %d)\n", stamp3);
});
}… mit dem folgenden Trace:
t1: optimisitically trying...
t1: begin opt. (stamp1 = 256)
t3: trying writeLock
t3: acquired writeLock (stamp3 = 384)
t1: validate opt. (stamp1 = 256)
t1: optimisitically trying...
t1: begin opt. (stamp1 = 0)
t3: released writeLock (stamp3 = 384)
t2: trying readLock
t2: acquired readLock (stamp2 = 513)
t1: validate opt. (stamp1 = 0)
t1: optimisitically trying...
t1: begin opt. (stamp1 = 512)
t2: released readLock (stamp2 = 513)
t1: validate opt. (stamp1 = 512)
t1: was valid (stamp1 = 512)
t1versucht optimistischlockzu nutzent3schließtlockexklusiv (t1bekommt davon erstmal nichts mit undt3wird auch nicht blockiert) \(\to\)lockblaut1ist fertig, stellt aber Misserfolg fest und versucht es erneut opt. (t1wird nicht blockiert, obwohlt3exkl. Lock hat)t3gibtlockwieder frei \(\to\)locknicht mehr blaut2fordert erfolgreich nicht-exkl. Lock an.lockist frei,t2blockiert nicht.locknun nicht-exkl. vergeben \(\to\)lockgelbt1ist fertig, stellt aber Misserfolg fest und versucht es erneut opt. (t1wird nicht blockiert)t2ist fertig \(\to\)lockist nicht mehr gelbt1ist fertig und stellt nun fest, dass das opt. Lesen endlich erfolgreich war
6.5 Semaphore: Koordination der konkurrierenden Nutzung von Ressourcen
Semaphore sind ein den Monitoren und Locks verwandtes Konzept. Im Gegensatz zu diesen haben sie aber eine Kapazität. Man kann damit den Grad der Parallelität steuern.
Für den konkurrierenden Zugriff auf Variablen sind sie nicht geeignet, da hier entweder beliebig viele parallele Zugriff erlaubt sind (ReadLock) oder nur exakt einer (gegenseitiger Ausschluss).
- Semaphore ermöglichen die Begrenzung der Nutzung einer Ressource auf eine bestimmte Anzahl Nutzer.
- Kapazität eines Semaphores (
permitCount): Anzahl an noch erlaubten Nutzern. - Operationen:
release()undacquire()(blockiert, fallspermitCount == 0)
Semaphore, die mit der Kapazität 1 angelegt werden, verhalten sich wie Locks (haben aber bei Java eine andere Schnittstelle).
