Leitfaden für java.util.concurrent.Locks

Anleitung zu java.util.concurrent.Locks

 

1. Überblick

Einfach ausgedrückt ist eine Sperre ein flexiblerer und ausgefeilterer Thread-Synchronisationsmechanismus als der Standardblocksynchronized.

DieLock-Schnittstelle gibt es seit Java 1.5. Es ist im Paketjava.util.concurrent.lockdefiniert und bietet umfangreiche Operationen zum Sperren.

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

2. Unterschiede zwischen Sperre und synchronisiertem Block

Es gibt nur wenige Unterschiede zwischen der Verwendung synchronisierterblock und der Verwendung vonLock APIs:

  • A synchronized block is fully contained within a method – können wirLock APIslock() undunlock() Operation in separaten Methoden haben

  • A synchronized block unterstützt die Fairness nicht. Jeder Thread kann die Sperre erhalten, sobald sie freigegeben wurde. Es kann keine Präferenz angegeben werden. We can achieve fairness within the Lock APIs by specifying the fairness property. 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 die synchronisiertenblockerhalten kann. The Lock API provides tryLock() method. The thread acquires lock only if it’s available and not held by any other thread. Dies reduziert die Blockierungszeit des Threads, der auf die Sperre wartet

  • Ein Thread, der sich im Status "Warten" befindet, um den Zugriff aufsynchronized block zu erhalten, kann nicht unterbrochen werden. The Lock API provides a method lockInterruptibly() which can be used to interrupt the thread when it’s waiting for the lock

3. Lock API

Werfen wir einen Blick auf die Methoden in derLock-Schnittstelle:

  • *void lock()* – erwerben 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 demlock(),, ermöglicht jedoch, dass der blockierte Thread unterbrochen wird und die Ausführung durch ein geworfenesjava.lang.InterruptedException fortgesetzt wird

  • boolean tryLock() - Dies ist eine nicht blockierende Version der Methodelock(). Es wird versucht, die Sperre sofort zu erhalten. Wenn die Sperre erfolgreich ist, wird true zurückgegeben

  • *boolean tryLock(long timeout, TimeUnit timeUnit)* – Dies ist ähnlich wietryLock(),, außer dass das angegebene Timeout abgewartet wird, bevor der Versuch aufgegeben wird,Lock zu erfassen

  • void unlock() - Schaltet die Instanz vonLockfrei

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

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

Zusätzlich zurLock-Schnittstelle, haben wir eineReadWriteLock-Schnittstelle, die ein Paar von Sperren verwaltet, eine für schreibgeschützte Operationen und eine für die Schreiboperation. Die Lesesperre kann gleichzeitig von mehreren Threads gehalten werden, solange nicht geschrieben wird.

ReadWriteLock deklariert Methoden zum Erfassen 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. Implementierungen sperren

4.1. ReentrantLock

Die KlasseReentrantLockimplementiert die SchnittstelleLock. Es bietet dieselbe Parallelität und Speichersemantik wie die implizite Monitorsperre, auf die mit den Methoden und Anweisungen vonsynchronizedzugegriffen wird, mit erweiterten Funktionen.

Mal sehen, wie wir die Synchronisation vonReenrtantLock fornutzen 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 Aufrufelock () undunlock() in den Blocktry-finally einschließen, um die Deadlock-Situationen zu vermeiden.

Mal sehen, wietryLock()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, dertryLock(), aufruft, eine Sekunde und gibt das Warten auf, wenn die Sperre nicht verfügbar ist.

4.2. ReentrantReadWriteLock

Die KlasseReentrantReadWriteLockimplementiert die SchnittstelleReadWriteLock.

Sehen wir uns Regeln zum Erfassen derReadLock oderWriteLock durch einen Thread an:

  • Read Lock - Wenn kein Thread die Schreibsperre erhalten oder angefordert hat, können mehrere Threads die Lesesperre erhalten

  • Write Lock - Wenn keine Threads lesen oder schreiben, kann nur ein Thread die Schreibsperre erhalten

Mal sehen, wie man dieReadWriteLock 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. Wenn kein Schreibvorgang ausgeführt wird, können mehrere Threads auf diesen Abschnitt zugreifen.

4.3. StampedLock

StampedLock wird in Java 8 eingeführt. Es werden auch Lese- und Schreibsperren unterstützt. Die Methoden zur Erfassung von Sperren geben jedoch einen Stempel zurück, mit dem eine Sperre aufgehoben 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);
        }
    }
}

Ein weiteres Merkmal vonStampedLock ist das optimistische Sperren. In den meisten Fällen müssen Lesevorgänge nicht auf den Abschluss des Schreibvorgangs warten. Daher ist die vollständige Lesesperre nicht erforderlich.

Stattdessen können wir auf Lesesperre upgraden:

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 mitConditions

Die KlasseConditionbietet 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 über die erforderlichen Bedingungen verfügt, um seinen Vorgang auszuführen. Ein Reader-Thread kann beispielsweise auf die Sperre einer gemeinsam genutzten Warteschlange zugreifen, für die noch keine Daten verbraucht werden müssen.

Traditionell bietet Javawait(), notify() and notifyAll()-Methoden für die Thread-Interkommunikation. Conditions 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 derLock-Schnittstelle und der neu eingeführtenStampedLock-Klasse gesehen. Wir haben auch untersucht, wie wir dieCondition-Klasse verwenden können, um mit mehreren Bedingungen zu arbeiten.

Der vollständige Code für dieses Tutorial istover on GitHub verfügbar.