Το Lock είναι ένας μηχανισμός συγχρονισμού που παρέχει αποκλειστική πρόσβαση σε κρίσιμες ενότητες κώδικα σε πολυνηματικές εφαρμογές. Σύμφωνα με την Oracle, 2024, η διεπαφή Lock παρέχει πιο ευέλικτο έλεγχο συγχρονισμού σε σύγκριση με τα παραδοσιακά synchronized μπλοκ, συμπεριλαμβανομένων προσπαθειών κλειδώματος με timeout και υποστήριξη πολλαπλών ουρών αναμονής.
Κύρια σημεία
Lock είναι μια διεπαφή από το πακέτο java.util.concurrent.locks που παρέχει ρητές λειτουργίες κλειδώματος και ξεκλειδώματος για συγχρονισμό πρόσβασης σε δεδομένα. Σε αντίθεση με το synchronized, το Lock δίνει στον προγραμματιστή πλήρη έλεγχο του μηχανισμού κλειδώματος.
Η διεπαφή Lock εμφανίστηκε στην Java 5 ως εναλλακτική λύση στον ενσωματωμένο μηχανισμό synchronized. Οι κύριες μέθοδοι είναι lock, unlock, tryLock και lockInterruptibly. Τα κλειδώματα επιτρέπουν την οργάνωση ασφαλούς πρόσβασης σε δεδομένα σε πολυνηματικό περιβάλλον, αποτρέποντας συνθήκες ανταγωνισμού και καταστροφή δεδομένων.
Το κύριο πλεονέκτημα του Lock έναντι του synchronized είναι η ευελιξία. Ο προγραμματιστής μπορεί να επιχειρήσει κλείδωμα με timeout, να ελέγξει την κατάληψη χωρίς αποκλεισμό ή να οργανώσει πολλαπλές ουρές αναμονής με διαφορετικές προτεραιότητες.
Πριν από την εμφάνιση της διεπαφής Lock στην Java 5, ο μόνος τρόπος συγχρονισμού ήταν το synchronized, το οποίο υπέφερε από περιορισμούς: έλλειψη timeouts, αδυναμία διακοπής αναμονής και μία μόνο ουρά. Ο Doug Lea σχεδίασε το πακέτο java.util.concurrent, συμπεριλαμβάνοντας το Lock ως θεμελιώδες δομικό στοιχείο.
Κλείδωμα διαχειρίζεται την πρόσβαση μέσω μιας εσωτερικής σημαίας κατάστασης και μιας ουράς αναμονής. Όταν ένα νήμα καλεί lock(), ο μηχανισμός ελέγχει αν το κλείδωμα είναι ελεύθερο και είτε το κλειδώνει είτε τοποθετεί το νήμα στην ουρά μέχρι την απελευθέρωση.
Στη βάση κάθε κλειδώματος βρίσκεται μια ατομική λειτουργία σύγκρισης και ρύθμισης (CAS). Κατά την κλήση lock(), το νήμα προσπαθεί να ρυθμίσει ατομικά τη σημαία κατάληψης. Εάν η σημαία είναι ήδη ρυθμισμένη, το νήμα μπλοκάρεται. Στο unlock(), η σημαία επαναφέρεται και ένα από τα νήματα που περιμένουν αφυπνίζεται.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// κρίσιμη ενότητα
println("Λειτουργεί το νήμα ${Thread.currentThread().name}")
} finally {
lock.unlock()
}
}
ReentrantLock χρησιμοποιεί εσωτερικά μια αμφίδρομη ουρά (CLH lock queue), όπου κάθε νήμα που περιμένει αντιπροσωπεύεται από έναν κόμβο. Όταν το κλείδωμα απελευθερώνεται, ο κεφαλικός κόμβος της ουράς αφυπνίζεται. Η λειτουργία fair (δίκαιη) εγγυάται σειρά FIFO, ενώ η unfair επιτρέπει σε ένα νέο νήμα να κλειδώσει πριν από τα αναμένοντα για αυξημένη απόδοση.
Στη σύγχρονη στοίβα Java υπάρχουν πολλές υλοποιήσεις κλειδωμάτων, η καθεμία βελτιστοποιημένη για συγκεκριμένα σενάρια. Η επιλογή του σωστού κλειδώματος επηρεάζει άμεσα την απόδοση και την αξιοπιστία μιας πολυνηματικής εφαρμογής.
ReentrantLock — η βασική και πιο συχνά χρησιμοποιούμενη υλοποίηση του Lock. Υποστηρίζει επανακλείδωμα από το ίδιο νήμα: εάν το νήμα κατέχει ήδη το κλείδωμα, μια επαναλαμβανόμενη κλήση lock() δεν το μπλοκάρει. Αυτό αποτρέπει το deadlock σε αναδρομικές κλήσεις.
Το ReadWriteLock χωρίζει τα κλειδώματα σε δύο λειτουργίες: ανάγνωση και εγγραφή. Πολλαπλά νήματα μπορούν ταυτόχρονα να κρατούν το κλείδωμα ανάγνωσης, αλλά η εγγραφή απαιτεί αποκλειστική πρόσβαση. Αυτό αυξάνει σημαντικά την απόδοση σε συχνή ανάγνωση και σπάνια εγγραφή.
StampedLock — η νεότερη υλοποίηση, που εμφανίστηκε στην Java 8. Υποστηρίζει τρεις λειτουργίες: εγγραφή, ανάγνωση και αισιόδοξη ανάγνωση. Η αισιόδοξη ανάγνωση δεν μπλοκάρει άλλα νήματα και ελέγχει την εγκυρότητα των δεδομένων μετά την ανάγνωση, παρέχοντας αύξηση απόδοσης 10-20% σε σύγκριση με το ReadWriteLock.
| Κλείδωμα | Έκδοση Java | Λειτουργίες | Απόδοση |
|---|---|---|---|
| ReentrantLock | Java 5 | αποκλειστικό | υψηλή |
| ReadWriteLock | Java 5 | ανάγνωση + εγγραφή | μεσαία |
| StampedLock | Java 8 | ανάγνωση + εγγραφή + optimistic | πολύ υψηλή |
ReentrantLock — η πιο δημοφιλής υλοποίηση του Lock, που παρέχει μια σειρά από δυνατότητες μη διαθέσιμες στο synchronized. Η κατανόηση των χαρακτηριστικών του είναι απαραίτητη για αποτελεσματική εργασία με πολυνηματικότητα.
Ο κατασκευαστής του ReentrantLock δέχεται την παράμετρο fair. Στο true, το κλείδωμα εγγυάται σειρά FIFO πρόσβασης, στο false είναι δυνατό το κλείδωμα από νέο νήμα πριν από τα αναμένοντα. Η δίκαιη λειτουργία αποτρέπει την πείνα, αλλά μειώνει την απόδοση κατά 10-20% λόγω πρόσθετης επιβάρυνσης για τη συντήρηση της ουράς.
Σε αντίθεση με το synchronized, το ReentrantLock υποστηρίζει tryLock με timeout. Εάν το κλείδωμα δεν μπορεί να αποκτηθεί εντός του καθορισμένου χρόνου, το νήμα συνεχίζει την εκτέλεση αντί να μπλοκάρεται επ' αόριστον. Η μέθοδος lockInterruptibly επιτρέπει τη διακοπή του νήματος που περιμένει μέσω Thread.interrupt().
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Κλείδωμα αποκτήθηκε")
} finally {
lock.unlock()
}
} else {
println("Αποτυχία απόκτησης κλειδώματος")
}
}
Το ReentrantLock υποστηρίζει πολλαπλές μεταβλητές συνθήκης μέσω της μεθόδου newCondition(). Κάθε Condition έχει τη δική της ουρά αναμονής, επιτρέποντας την οργάνωση πολύπλοκων σεναρίων αφύπνισης. Οι μέθοδοι await() και signal() αντικατέστησαν τις wait() και notify() από τα μπλοκ synchronized, αλλά με υποστήριξη πολλαπλών ουρών.
ReadWriteLock και StampedLock λύνουν το πρόβλημα βελτιστοποίησης πρόσβασης όταν οι λειτουργίες ανάγνωσης υπερισχύουν της εγγραφής. Είναι σημαντικά πιο αποδοτικά από το ReentrantLock σε σενάρια όπου η ανάγνωση συμβαίνει συχνότερα από την εγγραφή.
Η διεπαφή ReadWriteLock περιέχει δύο μεθόδους: readLock() και writeLock(). Το κλείδωμα ανάγνωσης μπορεί να κρατηθεί από πολλαπλά νήματα ταυτόχρονα, το κλείδωμα εγγραφής — μόνο από ένα. Ένα τυπικό παράδειγμα — μια προστατευμένη κρυφή μνήμη: πολλά νήματα διαβάζουν δεδομένα και μόνο ένα τα ενημερώνει περιοδικά.
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 προσθέτει μια τρίτη λειτουργία — tryOptimisticRead. Αυτή η λειτουργία δεν μπλοκάρει άλλα νήματα, απλά απομνημονεύει μια σφραγίδα (stamp) της κατάστασης. Μετά την ανάγνωση, ο προγραμματιστής καλεί validate(stamp) για να ελέγξει αν τα δεδομένα άλλαξαν κατά την ανάγνωση. Εάν τα δεδομένα άλλαξαν, η λειτουργία πρέπει να επαναληφθεί.
Στις κινητές εφαρμογές, τα κλειδώματα χρησιμοποιούνται για τον συντονισμό πρόσβασης σε κοινά δεδομένα μεταξύ νημάτων. Ωστόσο, η χρήση τους απαιτεί ιδιαίτερη προσοχή λόγω των περιορισμένων πόρων της συσκευής και της ανάγκης διατήρησης της ανταπόκρισης της διεπαφής.
Στο Android, το ReentrantLock είναι χρήσιμο κατά την εργασία με Room, κρυφές μνήμες και αρχεία. Είναι σημαντικό να θυμάστε: ποτέ μην κλειδώνετε στο κύριο νήμα. Για ασύγχρονο κώδικα, προτιμώνται οι κορουτίνες και το Mutex από το kotlinx.coroutines, που δεν μπλοκάρουν το νήμα αλλά αναστέλλουν την κορουτίνα.
Στο iOS, το τυπικό Lock από το NSLock χρησιμοποιείται λιγότερο συχνά — οι προγραμματιστές προτιμούν DispatchQueue με σημαίες barrier ή τα λειτουργικά κλειδώματα os_unfair_lock. Το Swift 5.7+ παρέχει σύγχρονους μηχανισμούς συγχρονισμού μέσω actors, που προστατεύουν αυτόματα την κατάσταση.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
Για να αποφύγετε το deadlock, τηρείτε ενιαία σειρά κλειδώματος όλων των κλειδωμάτων στο έργο. Χρησιμοποιήστε tryLock με timeout αντί για lock() παντού όπου είναι δυνατός ο μακροπρόθεσμος αποκλεισμός. Εξετάστε την εφαρμογή αλγορίθμων Lock-Free (AtomicReference, ConcurrentHashMap) αντί για παραδοσιακά κλειδώματα.
Η χρήση Lock απαιτεί πειθαρχία και τήρηση πολλών κανόνων που αποτρέπουν το deadlock και την πτώση απόδοσης. Αυτές οι πρακτικές αναπτύχθηκαν από την κοινότητα Java κατά τη διάρκεια 20 ετών χρήσης του πακέτου java.util.concurrent.
Το πιο σημαντικό μοτίβο — lock στο finally. Ανεξάρτητα από το αν η κρίσιμη ενότητα ολοκληρώθηκε με επιτυχία ή με εξαίρεση, το κλείδωμα πρέπει να απελευθερωθεί. Αυτό εγγυάται ότι άλλα νήματα δεν θα μπλοκαριστούν για πάντα λόγω ενός σφάλματος. Στο Kotlin, αυτό το μοτίβο λύνεται κομψά μέσω της επέκτασης withLock.
Η κρίσιμη ενότητα πρέπει να είναι μέγιστα σύντομη. Ποτέ μην εκτελείτε είσοδο-έξοδο, αιτήματα δικτύου ή μακροχρόνιους υπολογισμούς εντός του κλειδώματος. Εάν χρειάζεται να διαβάσετε δεδομένα από τον διακομιστή, πρώτα αποκτήστε τα, στη συνέχεια κλειδώστε μόνο για την ενημέρωση της κοινής κατάστασης. Αυτό μειώνει τον ανταγωνισμό και αυξάνει την απόδοση του συστήματος.
Για την αποτροπή deadlock κατά την εργασία με πολλαπλά Lock, καθορίστε παγκόσμια σειρά κλειδώματος σε ολόκληρο το έργο. Εάν πρώτα κλειδώνεται το lockA, στη συνέχεια το lockB — οποιαδήποτε αντίστροφη ακολουθία πρέπει να απαγορεύεται από τους κανόνες code review. Για αυτόματο έλεγχο, χρησιμοποιήστε στατικούς αναλυτές όπως SpotBugs και IntelliJ Inspections.
Συχνές ερωτήσεις
Lock — ρητή διεπαφή με δυνατότητα timeout και διακοπτόμενης αναμονής. Το synchronized κλειδώνει και ξεκλειδώνει αυτόματα τον οθόνη, αλλά δεν επιτρέπει τη χρήση tryLock, lockInterruptibly και πολλαπλών Condition. Το Lock είναι πιο ευέλικτο, αλλά απαιτεί χειροκίνητη απελευθέρωση στο finally.
Το δίκαιο κλείδωμα εγγυάται σειρά FIFO πρόσβασης: το νήμα που περιμένει περισσότερο λαμβάνει πρώτο το κλείδωμα. Το άδικο κλείδωμα μπορεί να δώσει πρόσβαση σε ένα νέο νήμα παρακάμπτοντας την ουρά, που αυξάνει την απόδοση αλλά μπορεί να προκαλέσει πείνα των νημάτων που περιμένουν.
Τηρείτε σταθερή σειρά κλειδώματος όλων των lock, χρησιμοποιήστε tryLock με timeout αντί για άνευ όρων lock και ελαχιστοποιήστε τον αριθμό των ταυτόχρονα κρατούμενων κλειδωμάτων. Η εφαρμογή δομών δεδομένων Lock-Free μειώνει επίσης τον κίνδυνο deadlock.
Condition — αντίστοιχο του wait/notify για το Lock, που επιτρέπει την οργάνωση πολλαπλών ανεξάρτητων ουρών αναμονής. Κάθε κλήση newCondition() δημιουργεί ξεχωριστή ουρά, παρέχοντας πιο ακριβή έλεγχο της αφύπνισης νημάτων σε σύγκριση με τη μοναδική ουρά του synchronized.
Για Android με κορουτίνες, χρησιμοποιήστε Mutex από το kotlinx.coroutines — αναστέλλει την κορουτίνα, όχι το νήμα. Για iOS με Swift 5.7+, προτιμώνται οι actors, που συγχρονίζουν αυτόματα την πρόσβαση στην κατάσταση. Αφήστε το ReentrantLock για παλαιό κώδικα και σενάρια χαμηλού επιπέδου.
Περίληψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης