Lock ist ein Synchronisationsmechanismus, der exklusiven Zugriff auf kritische Codeabschnitte in Multithread-Anwendungen bietet. Laut Oracle, 2024 bietet die Lock-Schnittstelle eine flexiblere Synchronisationskontrolle im Vergleich zu traditionellen synchronized-Blöcken, einschließlich Erfassungsversuchen mit Timeout und Unterstützung mehrerer Warteschlangen.
Wichtige Punkte
Lock ist eine Schnittstelle aus dem Paket java.util.concurrent.locks, die explizite Sperr- und Entsperroperationen zur Synchronisation des Datenzugriffs bereitstellt. Im Gegensatz zu synchronized gibt Lock dem Entwickler die vollständige Kontrolle über den Sperrmechanismus.
Die Lock-Schnittstelle wurde in Java 5 als Alternative zum integrierten synchronized-Mechanismus eingeführt. Die wichtigsten Methoden sind lock, unlock, tryLock und lockInterruptibly. Sperren ermöglichen einen sicheren Datenzugriff in einer Multithread-Umgebung und verhindern Wettlaufsituationen und Datenkorruption.
Der Hauptvorteil von Lock gegenüber synchronized ist die Flexibilität. Der Entwickler kann versuchen, eine Sperre mit Timeout zu erwerben, ihre Verfügbarkeit ohne Blockierung prüfen oder mehrere Warteschlangen mit unterschiedlichen Prioritäten organisieren.
Bevor die Lock-Schnittstelle in Java 5 erschien, war die einzige Synchronisationsmethode synchronized, das unter Einschränkungen litt: keine Timeouts, kein unterbrechbares Warten und eine einzige Warteschlange. Doug Lea entwarf das Paket java.util.concurrent und nahm Lock als grundlegenden Baustein auf.
Eine Sperre verwaltet den Zugriff über ein internes Zustandsflag und eine Warteschlange. Wenn ein Thread lock() aufruft, prüft der Mechanismus, ob die Sperre frei ist, und erwirbt sie entweder oder reiht den Thread ein, bis sie freigegeben wird.
Im Kern jeder Sperre liegt eine atomare Compare-and-Swap (CAS)-Operation. Beim Aufruf von lock() versucht der Thread, das Belegt-Flag atomar zu setzen. Ist das Flag bereits gesetzt, wird der Thread blockiert. Bei unlock() wird das Flag gelöscht und ein wartender Thread wird aufgeweckt.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// kritischer Abschnitt
println("Thread ${Thread.currentThread().name} arbeitet")
} finally {
lock.unlock()
}
}
ReentrantLock verwendet intern eine doppelt verkettete Liste (CLH-Sperrwarteschlange), in der jeder wartende Thread durch einen Knoten dargestellt wird. Wenn die Sperre freigegeben wird, wird der Kopfknoten der Warteschlange aufgeweckt. Der faire Modus garantiert FIFO-Reihenfolge, während der unfaire Modus einem neuen Thread erlaubt, die Sperre vor wartenden Threads zu erwerben, um den Durchsatz zu erhöhen.
Im modernen Java-Ökosystem gibt es mehrere Sperren-Implementierungen, die jeweils für bestimmte Szenarien optimiert sind. Die Wahl der richtigen Sperre wirkt sich direkt auf die Leistung und Zuverlässigkeit einer Multithread-Anwendung aus.
ReentrantLock ist die grundlegende und am häufigsten verwendete Lock-Implementierung. Sie unterstützt die Wiedererfassung durch denselben Thread: Wenn ein Thread die Sperre bereits hält, blockiert ein erneuter Aufruf von lock() nicht. Dies verhindert Deadlocks bei rekursiven Aufrufen.
ReadWriteLock trennt Sperren in zwei Modi: Lesen und Schreiben. Mehrere Threads können die Lesesperre gleichzeitig halten, aber Schreiben erfordert exklusiven Zugriff. Dies verbessert die Leistung bei häufigem Lesen und seltenem Schreiben erheblich.
StampedLock ist die neueste Implementierung, die in Java 8 eingeführt wurde. Sie unterstützt drei Modi: Schreiben, Lesen und optimistisches Lesen. Optimistisches Lesen blockiert andere Threads nicht und validiert die Daten nach dem Lesen, was eine Leistungssteigerung von 10-20% gegenüber ReadWriteLock bietet.
| Sperre | Java-Version | Modi | Leistung |
|---|---|---|---|
| ReentrantLock | Java 5 | exklusiv | hoch |
| ReadWriteLock | Java 5 | Lesen + Schreiben | mittel |
| StampedLock | Java 8 | Lesen + Schreiben + optimistisch | sehr hoch |
ReentrantLock ist die beliebteste Lock-Implementierung und bietet mehrere Funktionen, die in synchronized nicht verfügbar sind. Das Verständnis ihrer Eigenschaften ist für effektive Multithread-Arbeit unerlässlich.
Der ReentrantLock-Konstruktor akzeptiert einen fair-Parameter. Bei true garantiert die Sperre FIFO-Reihenfolge; bei false kann ein neuer Thread die Sperre vor wartenden Threads erwerben. Der faire Modus verhindert Aushungerung, reduziert aber den Durchsatz um 10-20% aufgrund des Overheads der Warteschlangenverwaltung.
Im Gegensatz zu synchronized unterstützt ReentrantLock tryLock mit Timeout. Wenn die Sperre nicht innerhalb der angegebenen Zeit erworben werden kann, setzt der Thread die Ausführung fort, anstatt unbegrenzt zu blockieren. Die Methode lockInterruptibly ermöglicht es, einen wartenden Thread über Thread.interrupt() zu unterbrechen.
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Sperre erworben")
} finally {
lock.unlock()
}
} else {
println("Sperre konnte nicht erworben werden")
}
}
ReentrantLock unterstützt mehrere Bedingungsvariablen über die Methode newCondition(). Jede Condition hat ihre eigene Warteschlange, was komplexe Aufweckszenarien ermöglicht. Die Methoden await() und signal() ersetzten wait() und notify() aus synchronized-Blöcken, aber mit Unterstützung für mehrere Warteschlangen.
ReadWriteLock und StampedLock adressieren die Optimierung des Zugriffs, wenn Lesevorgänge gegenüber Schreibvorgängen überwiegen. Sie sind in Szenarien, in denen Lesen häufiger vorkommt als Schreiben, deutlich effizienter als ReentrantLock.
Die ReadWriteLock-Schnittstelle enthält zwei Methoden: readLock() und writeLock(). Die Lesesperre kann von mehreren Threads gleichzeitig gehalten werden, während die Schreibsperre exklusiv ist. Ein typisches Beispiel ist ein threadsicherer Cache: Viele Threads lesen Daten, während nur einer sie regelmäßig aktualisiert.
class SafeCache<K, V> {
private val map = mutableMapOf<K, V>()
private val rwLock = ReentrantReadWriteLock()
fun get(key: K): V? {
rwLock.readLock().lock()
return try { map[key] } finally { rwLock.readLock().unlock() }
}
fun put(key: K, value: V) {
rwLock.writeLock().lock()
return try { map[key] = value } finally { rwLock.writeLock().unlock() }
}
}
StampedLock fügt einen dritten Modus hinzu — tryOptimisticRead. Dieser Modus blockiert andere Threads nicht, sondern zeichnet lediglich einen Zustandsstempel (stamp) auf. Nach dem Lesen ruft der Entwickler validate(stamp) auf, um zu prüfen, ob sich die Daten während des Lesens geändert haben. Wenn sich die Daten geändert haben, muss der Vorgang wiederholt werden.
In mobilen Anwendungen werden Sperren verwendet, um den Zugriff auf gemeinsam genutzte Daten zwischen Threads zu koordinieren. Ihre Verwendung erfordert jedoch besondere Vorsicht aufgrund der begrenzten Geräteressourcen und der Notwendigkeit, die UI-Reaktionsfähigkeit zu erhalten.
Auf Android ist ReentrantLock bei der Arbeit mit Room, Caches und Dateien nützlich. Wichtig zu beachten: Erwerben Sie niemals eine Sperre im Hauptthread. Für asynchronen Code sind Koroutinen und Mutex aus kotlinx.coroutines vorzuziehen, da sie die Koroutine aussetzen, anstatt den Thread zu blockieren.
In iOS wird das standardmäßige NSLock seltener verwendet — Entwickler bevorzugen DispatchQueue mit Barrier-Flags oder os_unfair_lock. Swift 5.7+ bietet moderne Synchronisationsmechanismen durch Actors, die den Zustand automatisch schützen.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
Um Deadlocks zu vermeiden, befolgen Sie eine einheitliche Sperrreihenfolge im gesamten Projekt. Verwenden Sie tryLock mit Timeout anstelle von lock(), wo immer längeres Blockieren möglich ist. Erwägen Sie die Verwendung von Lock-Free-Algorithmen (AtomicReference, ConcurrentHashMap) anstelle traditioneller Sperren.
Die Verwendung von Lock erfordert Disziplin und die Einhaltung mehrerer Regeln, die Deadlocks und Leistungseinbußen verhindern. Diese Praktiken wurden von der Java-Community in 20 Jahren Nutzung des Pakets java.util.concurrent entwickelt.
Das wichtigste Muster ist lock in finally. Unabhängig davon, ob der kritische Abschnitt erfolgreich abgeschlossen wird oder eine Ausnahme auslöst, muss die Sperre freigegeben werden. Dies stellt sicher, dass andere Threads aufgrund eines einzelnen Fehlers nicht für immer blockiert werden. In Kotlin wird dieses Muster elegant durch die withLock-Erweiterung gelöst.
Der kritische Abschnitt sollte so kurz wie möglich sein. Führen Sie niemals E/A, Netzwerkanfragen oder lange Berechnungen innerhalb einer Sperre durch. Wenn Sie Daten von einem Server lesen müssen, holen Sie sie zuerst ab und erwerben Sie die Sperre nur zur Aktualisierung des gemeinsamen Zustands. Dies reduziert Konflikte und verbessert den Systemdurchsatz.
Um Deadlocks bei der Arbeit mit mehreren Sperren zu verhindern, legen Sie eine globale Sperrreihenfolge für das gesamte Projekt fest. Wenn zuerst lockA und dann lockB erworben wird, muss jede umgekehrte Reihenfolge durch Code-Review-Regeln verboten werden. Verwenden Sie statische Analyzer wie SpotBugs und IntelliJ Inspections zur automatischen Überprüfung.
Häufig gestellte Fragen
Lock ist eine explizite Schnittstelle mit Timeout- und unterbrechbarem Wartesupport. synchronized erwirbt und gibt den Monitor automatisch frei, erlaubt aber nicht die Verwendung von tryLock, lockInterruptibly oder mehreren Conditions. Lock ist flexibler, erfordert aber manuelle Freigabe in finally.
Eine faire Sperre garantiert FIFO-Reihenfolge: Der Thread, der am längsten wartet, erhält zuerst die Sperre. Eine unfaire Sperre kann einem neuen Thread Zugriff vor wartenden Threads gewähren, was den Durchsatz erhöht, aber bei wartenden Threads zu Aushungerung führen kann.
Befolgen Sie eine feste Reihenfolge für den Erwerb aller Sperren, verwenden Sie tryLock mit Timeout anstelle von bedingungslosem lock() und minimieren Sie die Anzahl gleichzeitig gehaltener Sperren. Die Verwendung von Lock-Free-Datenstrukturen reduziert ebenfalls das Deadlock-Risiko.
Condition ist das Analogon zu wait/notify für Lock und ermöglicht mehrere unabhängige Warteschlangen. Jeder newCondition()-Aufruf erstellt eine separate Warteschlange und bietet eine präzisere Kontrolle über die Thread-Aufweckung im Vergleich zur einzigen Warteschlange von synchronized.
Für Android mit Koroutinen verwenden Sie Mutex aus kotlinx.coroutines — es setzt die Koroutine aus, anstatt den Thread zu blockieren. Für iOS mit Swift 5.7+ sind Actors vorzuziehen, die den Zustandszugriff automatisch synchronisieren. Lassen Sie ReentrantLock für Legacy-Code und Low-Level-Szenarien.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch