W3docs

Java Lock Interface

Das java.util.concurrent.locks.Lock-Interface — was es gegenüber `synchronized` bietet und die Regeln für den sicheren Einsatz.

synchronized ist das kleine, präzise Werkzeug. Es ist schnell, automatisch und deckt die meisten Anforderungen an gegenseitigen Ausschluss ab. Aber wenn man es überwächst — wenn man ein Timeout, eine Abbruchmöglichkeit oder mehr als eine Bedingungsvariable benötigt — bietet Java eine zweite, reichhaltigere Sperr-API: das java.util.concurrent.locks.Lock-Interface und seine Implementierungen. Dieses Kapitel stellt das Interface vor; die nächsten beiden Kapitel behandeln die zwei Implementierungen (ReentrantLock, ReentrantReadWriteLock), die man tatsächlich verwenden wird.

Was das Interface bietet

public interface Lock {
  void lock();
  void lockInterruptibly() throws InterruptedException;
  boolean tryLock();
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  void unlock();
  Condition newCondition();
}

Sechs Methoden. Fünf davon betreffen das Anfordern oder Freigeben der Sperre; eine gibt ein Condition-Objekt zurück (die Antwort von Lock auf wait/notify).

Die vier Arten des Erwerbs sind das, was synchronized nicht bietet:

  • lock() — blockieren, bis erworben. Am nächsten an synchronized.
  • lockInterruptibly() — blockieren, bis erworben, aber mit InterruptedException abbrechen, wenn unterbrochen. Ermöglicht das Abbrechen eines Threads, der auf eine Sperre wartet.
  • tryLock() — einmal versuchen, sofort true/false zurückgeben. Blockiert nicht.
  • tryLock(time, unit) — bis zu einem Timeout versuchen, dann aufgeben. Das Werkzeug zur Deadlock-Prävention aus dem vorletzten Kapitel.

synchronized hat nur einen Erwerbsmodus — für immer blockieren, bis man die Sperre erhält. Das ist für den Großteil des Codes angemessen; nicht jedoch, wenn man eine Frist oder einen Abbruchpunkt benötigt.

Das obligatorische try/finally-Muster

synchronized gibt den Monitor automatisch frei, wenn der Block verlassen wird — bei normalem Abschluss oder bei einer Ausnahme. Lock tut das nicht. Wenn man vergisst, unlock aufzurufen, wird die Sperre für immer gehalten und alles danach blockiert.

Das richtige Muster, jedes Mal:

lock.lock();
try {
  // critical section
} finally {
  lock.unlock();
}

unlock muss in einem finally stehen, damit es auch bei Ausnahmen im Rumpf ausgeführt wird. Es gibt kein Try-with-Resources für Lock direkt (es ist nicht AutoCloseable), aber es gibt Wrapper-Muster, die das nachahmen. Das obige Standardmuster ist das, was fast der gesamte Produktionscode verwendet.

tryLock und Timeout

Die beiden tryLock-Überladungen sind die Art, wie Lock den Umgang mit "Was, wenn wir die Sperre nicht bekommen können?" ermöglicht:

if (lock.tryLock()) {
  try {
    doWork();
  } finally {
    lock.unlock();
  }
} else {
  // didn't get the lock — do something else, maybe retry later
}
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {       // wait up to 500ms
  try {
    doWork();
  } finally {
    lock.unlock();
  }
} else {
  throw new TimeoutException("couldn't acquire " + name);
}

Die zweite Form ist das, was Deadlock-Recovery möglich macht. Mit synchronized wartet ein Thread, der auf einen Monitor wartet, bis der Halter freigibt — es gibt keinen Ausweg außer dem Absturz der JVM. Mit tryLock(timeout) gibt man nach einer Frist auf und wiederholt entweder, lässt die Operation fehlschlagen oder nimmt einen alternativen Pfad.

lockInterruptibly — abbrechbarer Sperrerwerb

synchronized reagiert während des Wartens nicht auf Thread.interrupt(). Ein Thread, der BLOCKED auf einem Monitor ist, bleibt blockiert, auch wenn man ihn unterbricht — die JVM setzt lediglich das Flag und vergisst es.

lock.lockInterruptibly() reagiert darauf. Wenn ein anderer Thread interrupt() auf einen Thread aufruft, während dieser auf die Sperre wartet, löst der Aufruf sofort InterruptedException aus:

try {
  lock.lockInterruptibly();
} catch (InterruptedException e) {
  Thread.currentThread().interrupt();
  return;                                              // gave up on the work
}
try {
  doWork();
} finally {
  lock.unlock();
}

Dies ist in Server-Code unerlässlich: Eine Anfrage kommt herein, ein Thread versucht eine Sperre zu erwerben, die Anfrage wird abgebrochen (Client-Trennung, Timeout von einem Load-Balancer), der Supervisor ruft interrupt() auf dem Worker auf. Mit synchronized wartet der Worker weiter; mit lockInterruptibly gibt er auf.

Condition — das Lock-bewusste wait/notify

Das Äquivalent zum intrinsischen Monitor — wait/notify auf einem synchronized-Block — gibt einem genau eine Warteschlange pro Objekt. Ein einziges Lock kann mehrere Condition-Objekte haben:

Lock lock = new ReentrantLock();
Condition notFull  = lock.newCondition();
Condition notEmpty = lock.newCondition();

Man hält die Sperre, await()et auf einer Bedingung (was die Sperre freigibt und einen parkiert), und ein anderer Thread signal()t die Bedingung (was einen in den BLOCKED-Zustand versetzt, in dem man auf die Sperre wartet). Die Entsprechung zu wait/notify:

Lock + ConditionIntrinsischer Monitor
lock.lock()synchronized (obj) betreten
condition.await()obj.wait()
condition.signal()obj.notify()
condition.signalAll()obj.notifyAll()
lock.unlock()synchronized verlassen

Der Vorteil gegenüber wait/notify: mehrere Bedingungen pro Sperre. Ein begrenzter Puffer kann eine Bedingung für "nicht voll" und eine für "nicht leer" haben — Produzenten signal(notEmpty) nach dem Einlegen eines Elements; Konsumenten signal(notFull) nach dem Entnehmen. Nur die richtige Seite wird geweckt. Der Ansatz mit notifyAll auf einem einzelnen Monitor muss alle aufwecken und hofft.

Wir werden die Neuschreibung des begrenzten Puffers im ReentrantLock-Kapitel sehen.

Wann Lock verwenden, wann bei synchronized bleiben

Eine pragmatische Entscheidungsregel:

  • Standardmäßig synchronized für einfachen gegenseitigen Ausschluss. Es ist automatisch, kann nicht verloren gehen, und die JVM optimiert es stark.
  • Lock verwenden, wenn man Folgendes benötigt: ein Timeout beim Erwerb, die Möglichkeit, einen Wartenden über interrupt abzubrechen, mehrere Conditions für dieselbe Sperre oder eine Lese-Schreib-Unterscheidung (ReentrantReadWriteLock).
  • Lock verwenden bei hohem Konfliktniveau und wenn man eine faire Reihenfolge benötigt (new ReentrantLock(true) ist die faire Version; intrinsische Monitore sind unfair). Faire Reihenfolge tauscht Durchsatz gegen Vorhersagbarkeit.

Man sollte synchronized nicht ohne Grund auf Lock "upgraden". Beide sind für den Grundfall gleichwertig; der Rest des Kapitels zeigt, wann die zusätzlichen Fähigkeiten sich auszahlen.

Was man aufgibt

Lock hat Kosten, die synchronized nicht hat:

  • Keine automatische Freigabe. finally vergessen und die Sperre verliert sich. Die JVM kann nicht helfen.
  • Keine strukturierte Verschachtelungsüberprüfung. Mit synchronized erzwingt der Compiler die Paarung von Sperren/Entsperren; mit Lock kann man unlock() aus einer anderen Methode oder einem anderen Pfad aufrufen, und der Compiler bemerkt es nicht.
  • Keine nativen Laufzeitoptimierungen. Die JVM hat spezielle Optimierungen für intrinsische Monitore (Biased Locking, Lock Coarsening, Lock Elision in einigen Fällen), die nicht für Lock gelten. Bei sehr niedrigem Konfliktaufkommen kann synchronized geringfügig schneller sein.
  • Mehr Oberfläche für Missbrauch. tryLock und lockInterruptibly müssen beide mit einer Prüfung gepaart werden; fehlt die Prüfung, entsteht ein stiller "Sperre nicht erworben"-Fehler.

Lock für die Fähigkeiten verwenden, nicht für die Syntax.

Ein ausgearbeitetes Beispiel: Lock macht, was synchronized nicht kann

Das Programm unten verwendet ReentrantLock (die Standard-Lock-Implementierung), um die drei Dinge zu demonstrieren, die synchronized nicht bietet: tryLock mit Timeout, lockInterruptibly und eine benutzerdefinierte Condition.

java— editable, runs on the server

Was man aus dem Lauf mitnehmen kann:

  • Das try/finally-Muster aus Abschnitt 1 ist das, was jede Lock-Aufrufstelle benötigt. Es gibt keinen syntaktischen Schutz — löscht man das finally, kompiliert der Code, und die Sperre verliert sich beim ersten Mal, wenn der Rumpf eine Ausnahme wirft. Die Form einprägen: lock(), try { ... } finally { unlock(); }.
  • Das tryLock(100, MS) aus Abschnitt 2 gab false nach ungefähr 100 ms zurück, weil der Halter-Thread noch in seinem 500 ms-Schlaf war. Das ist der Fristvertrag — der Aufruf gibt nach dem Timeout false zurück, egal was. Mit synchronized hätte dieser Thread blockiert, bis der Halter freigegeben hätte, ohne Ausweichmöglichkeit.
  • Der Wartende aus Abschnitt 3 wurde unterbrochen, während er auf die Sperre wartete, und lockInterruptibly warf InterruptedException. Zum Vergleich: lock.lock() oder synchronized — keines reagiert auf interrupt() während des Wartens. Das ist der Unterschied zwischen einem Server, der abgelaufene Anfragen abbrechen kann, und einem, der einfach blockierte Threads ansammelt.
  • Abschnitt 4 verwendete zwei Conditions auf einer Sperre — notFull für Produzenten, notEmpty für Konsumenten. Wenn der Produzent ein Element hinzufügte, signalte er gezielt notEmpty; nur ein Konsument wurde geweckt. Mit wait/notifyAll auf einem intrinsischen Monitor wird jeder wartende Thread geweckt und prüft erneut; das Condition-Paar sendet das Aufwecksignal an die richtige Seite der Warteschlange und spart den Aufweck-/Neuprüfungs-Roundtrip.
  • Das signal() (Singular) statt signalAll() ist hier sicher, weil alle awaiter auf jeder Bedingung austauschbar sind — jeder Produzent kann den Slot füllen, den wir gerade geöffnet haben. Wenn die Wartenden nicht austauschbar wären (z. B. wenn sie auf verschiedene spezifische Schlüssel warteten), wäre signalAll immer noch die sicherere Standardwahl.

Was als nächstes kommt

Das nächste Kapitel, Java ReentrantLock, geht im Detail auf die Standard-Lock-Implementierung ein — ihre Wiedereintrittsfähigkeit, ihre Fairness-Richtlinie und die diagnostische API getHoldCount/isHeldByCurrentThread.

Übungen

Übung
Du schreibst Code, der eine Sperre mit einer Frist erwerben muss — nach 200 ms aufgeben, wenn die Sperre nicht verfügbar ist, und stattdessen eine alternative Aktion ausführen. Welcher Ansatz ist der richtige?
Du schreibst Code, der eine Sperre mit einer Frist erwerben muss — nach 200 ms aufgeben, wenn die Sperre nicht verfügbar ist, und stattdessen eine alternative Aktion ausführen. Welcher Ansatz ist der richtige?
Was this page helpful?