Anleitung zu java.util.concurrent.Locks

1. Übersicht

Einfach ausgedrückt ist eine Sperre ein flexiblerer und ausgefeilterer Thread-Synchronisationsmechanismus als der standardmäßige synchronisierte Block.

Die Lock- Oberfläche gibt es seit Java 1.5. Es ist im Paket java.util.concurrent.lock definiert und bietet umfangreiche Operationen zum Sperren.

In diesem Artikel werden verschiedene Implementierungen der Lock- Schnittstelle und ihrer Anwendungen untersucht.

2. Unterschiede zwischen Sperre und synchronisiertem Block

Es gibt nur wenige Unterschiede zwischen der Verwendung eines synchronisierten Blocks und der Verwendung von Lock- APIs:

  • Ein synchronisierter Block ist vollständig in einer Methode enthalten. Die Lock () - und Unlock () -Operationen der Lock- API können in separaten Methoden ausgeführt werden
  • Ein synchronisierter Block unterstützt die Fairness nicht. Jeder Thread kann die Sperre erhalten, sobald er freigegeben wurde. Es kann keine Präferenz angegeben werden. Wir können Fairness innerhalb der Lock- APIs erreichen, indem wir die Fairness- Eigenschaft angeben . Es stellt sicher, dass der am längsten wartende Thread Zugriff auf die Sperre erhält
  • Ein Thread wird blockiert, wenn er keinen Zugriff auf den synchronisierten Block erhält . Die Lock- API bietet die tryLock () -Methode. Der Thread erhält nur dann eine Sperre, wenn er verfügbar ist und von keinem anderen Thread gehalten wird. Dies reduziert die Blockierungszeit des Threads, der auf die Sperre wartet
  • Ein Thread, der sich im Status "Warten" befindet, um den Zugriff auf den synchronisierten Block zu erhalten , kann nicht unterbrochen werden. Die Lock- API bietet eine Methode lockInterruptibly (), mit der der Thread unterbrochen werden kann, wenn er auf die Sperre wartet

3. Sperren API

Werfen wir einen Blick auf die Methoden in der Lock- Oberfläche:

  • void lock () - erwirbt die Sperre, falls verfügbar; Wenn die Sperre nicht verfügbar ist, wird ein Thread blockiert, bis die Sperre aufgehoben wird
  • void lockInterruptibly () - Dies ähnelt dem lock (), ermöglicht jedoch, dass der blockierte Thread unterbrochen wird und die Ausführung über eine ausgelöste java.lang.InterruptedException fortgesetzt wird
  • boolean tryLock () - Dies ist eine nicht blockierende Version der lock () -Methode. Es wird versucht, die Sperre sofort zu erhalten. Wenn die Sperre erfolgreich ist, wird true zurückgegeben
  • boolean tryLock (lange Zeitüberschreitung, TimeUnit timeUnit) - Dies ähnelt tryLock (), wartet jedoch auf die angegebene Zeitüberschreitung, bevor der Versuch, die Sperre zu erhalten, aufgegeben wird
  • Leere () entriegeln - entriegelt die Sperre Instanz

Eine gesperrte Instanz sollte immer entsperrt werden, um einen Deadlock-Zustand zu vermeiden. Ein empfohlener Codeblock zur Verwendung der Sperre sollte einen try / catch- und schließlich- Block enthalten:

Lock lock = ...; lock.lock(); try { // access to the shared resource } finally { lock.unlock(); }

Neben der Lock - Schnittstelle , haben wir eine ReadWriteLock Schnittstelle , die ein Paar von Schlössern, ein für Nur - Lese-Operationen, und ein für den Schreibvorgang unterhält. Die Lesesperre kann gleichzeitig von mehreren Threads gehalten werden, solange kein Schreibvorgang erfolgt.

ReadWriteLock deklariert Methoden zum Abrufen von Lese- oder Schreibsperren:

  • Lock readLock () - Gibt die Sperre zurück, die zum Lesen verwendet wird
  • Lock writeLock () - Gibt die Sperre zurück, die zum Schreiben verwendet wird

4. Implementieren sperren

4.1. ReentrantLock

Die ReentrantLock- Klasse implementiert die Lock- Schnittstelle. Es bietet dieselbe Parallelität und Speichersemantik wie die implizite Monitorsperre, auf die mit synchronisierten Methoden und Anweisungen zugegriffen wird , mit erweiterten Funktionen.

Mal sehen, wie wir ReenrtantLock für die Synchronisation verwenden können:

public class SharedObject { //... ReentrantLock lock = new ReentrantLock(); int counter = 0; public void perform() { lock.lock(); try { // Critical section here count++; } finally { lock.unlock(); } } //... }

Wir müssen sicherstellen, dass wir die Aufrufe lock () und entsperren () in den try-finally- Block einschließen, um die Deadlock-Situationen zu vermeiden.

Mal sehen, wie tryLock () funktioniert:

public void performTryLock(){ //... boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS); if(isLockAcquired) { try { //Critical section here } finally { lock.unlock(); } } //... } 

In diesem Fall wartet der Thread, der tryLock () aufruft, eine Sekunde und gibt das Warten auf, wenn die Sperre nicht verfügbar ist.

4.2. ReentrantReadWriteLock

Die ReentrantReadWriteLock- Klasse implementiert die ReadWriteLock- Schnittstelle.

Sehen wir uns die Regeln für den Erwerb von ReadLock oder WriteLock durch einen Thread an:

  • Lesesperre - Wenn kein Thread die Schreibsperre erworben oder angefordert hat, können mehrere Threads die Lesesperre erwerben
  • Schreibsperre - Wenn keine Threads lesen oder schreiben, kann nur ein Thread die Schreibsperre erhalten

Mal sehen, wie man das ReadWriteLock nutzt :

public class SynchronizedHashMapWithReadWriteLock { Map syncHashMap = new HashMap(); ReadWriteLock lock = new ReentrantReadWriteLock(); // ... Lock writeLock = lock.writeLock(); public void put(String key, String value) { try { writeLock.lock(); syncHashMap.put(key, value); } finally { writeLock.unlock(); } } ... public String remove(String key){ try { writeLock.lock(); return syncHashMap.remove(key); } finally { writeLock.unlock(); } } //... }

Für beide Schreibmethoden müssen wir den kritischen Abschnitt mit der Schreibsperre umgeben, nur ein Thread kann darauf zugreifen:

Lock readLock = lock.readLock(); //... public String get(String key){ try { readLock.lock(); return syncHashMap.get(key); } finally { readLock.unlock(); } } public boolean containsKey(String key) { try { readLock.lock(); return syncHashMap.containsKey(key); } finally { readLock.unlock(); } }

Für beide Lesemethoden müssen wir den kritischen Abschnitt mit der Lesesperre umgeben. Mehrere Threads können auf diesen Abschnitt zugreifen, wenn kein Schreibvorgang ausgeführt wird.

4.3. StampedLock

StampedLock wird in Java 8 eingeführt. Es unterstützt auch Lese- und Schreibsperren. Sperrenerfassungsmethoden geben jedoch einen Stempel zurück, mit dem eine Sperre freigegeben oder überprüft wird, ob die Sperre noch gültig ist:

public class StampedLockDemo { Map map = new HashMap(); private StampedLock lock = new StampedLock(); public void put(String key, String value){ long stamp = lock.writeLock(); try { map.put(key, value); } finally { lock.unlockWrite(stamp); } } public String get(String key) throws InterruptedException { long stamp = lock.readLock(); try { return map.get(key); } finally { lock.unlockRead(stamp); } } }

Eine weitere Funktion von StampedLock ist das optimistische Sperren. Die meisten Lesevorgänge müssen nicht auf den Abschluss des Schreibvorgangs warten. Daher ist die vollständige Lesesperre nicht erforderlich.

Stattdessen können wir ein Upgrade durchführen, um die Sperre zu lesen:

public String readWithOptimisticLock(String key) { long stamp = lock.tryOptimisticRead(); String value = map.get(key); if(!lock.validate(stamp)) { stamp = lock.readLock(); try { return map.get(key); } finally { lock.unlock(stamp); } } return value; }

5. Arbeiten mit Bedingungen

Die Condition- Klasse bietet einem Thread die Möglichkeit, auf das Auftreten einer Bedingung zu warten, während der kritische Abschnitt ausgeführt wird.

Dies kann auftreten, wenn ein Thread den Zugriff auf den kritischen Abschnitt erhält, jedoch nicht die erforderliche Bedingung für die Ausführung seines Vorgangs hat. Beispielsweise kann ein Reader-Thread auf die Sperre einer gemeinsam genutzten Warteschlange zugreifen, für die noch keine Daten zu verbrauchen sind.

Traditionell bietet Java die Methoden wait (), notify () und notifyAll () für die Thread-Interkommunikation. Bedingungen haben ähnliche Mechanismen, aber zusätzlich können wir mehrere Bedingungen angeben:

public class ReentrantLockWithCondition { Stack stack = new Stack(); int CAPACITY = 5; ReentrantLock lock = new ReentrantLock(); Condition stackEmptyCondition = lock.newCondition(); Condition stackFullCondition = lock.newCondition(); public void pushToStack(String item){ try { lock.lock(); while(stack.size() == CAPACITY) { stackFullCondition.await(); } stack.push(item); stackEmptyCondition.signalAll(); } finally { lock.unlock(); } } public String popFromStack() { try { lock.lock(); while(stack.size() == 0) { stackEmptyCondition.await(); } return stack.pop(); } finally { stackFullCondition.signalAll(); lock.unlock(); } } }

6. Fazit

In diesem Artikel haben wir verschiedene Implementierungen der Lock- Schnittstelle und der neu eingeführten StampedLock- Klasse gesehen. Wir haben auch untersucht, wie wir die Condition- Klasse verwenden können, um mit mehreren Bedingungen zu arbeiten.

Der vollständige Code für dieses Tutorial ist auf GitHub verfügbar.