Το Synchronized είναι ένας ενσωματωμένος μηχανισμός συγχρονισμού στη γλώσσα Java, που παρέχει αποκλειστική πρόσβαση σε κρίσιμες ενότητες κώδικα. Σύμφωνα με την Oracle, 2024, ο τροποποιητής synchronized εγγυάται ότι μόνο ένα νήμα μπορεί να εκτελέσει την επισημασμένη μέθοδο ή μπλοκ σε μια συγκεκριμένη χρονική στιγμή. Αυτός ο μηχανισμός βασίζεται σε οθόνες — μια θεμελιώδη έννοια των λειτουργικών συστημάτων, που διασφαλίζει τη σωστή λειτουργία πολυνηματικών εφαρμογών σε όλα τα επίπεδα πολυπλοκότητας.
Κύρια σημεία
Synchronized είναι μια λέξη-κλειδί στη Java που εγγυάται ότι μόνο ένα νήμα κάθε φορά εκτελεί το προστατευμένο τμήμα κώδικα, αποτρέποντας τη φθορά δεδομένων κατά την παράλληλη πρόσβαση. Εμφανίστηκε στην πρώτη έκδοση της Java και παραμένει ο απλούστερος τρόπος διασφάλισης της νηματικής ασφάλειας για προγραμματιστές όλων των επιπέδων.
Ο τροποποιητής synchronized λύνει δύο προβλήματα: αμοιβαίο αποκλεισμό (mutual exclusion) και ορατότητα αλλαγών (visibility). Όταν ένα νήμα εγκαταλείπει ένα synchronized μπλοκ, όλες οι αλλαγές είναι εγγυημένα ορατές σε άλλα νήματα που εισέρχονται στο μπλοκ που έχει συγχρονιστεί στο ίδιο αντικείμενο.
Το Synchronized μπορεί να εφαρμοστεί σε ολόκληρη τη μέθοδο ή σε ένα αυθαίρετο μπλοκ κώδικα με καθορισμό του αντικειμένου-οθόνης. Και στις δύο περιπτώσεις, η JVM εισάγει τις εντολές monitorenter και monitorexit σε επίπεδο bytecode.
Σε πολυνηματικές εφαρμογές χωρίς συγχρονισμό, προκύπτει συνθήκη ανταγωνισμού (race condition) — όταν δύο νήματα τροποποιούν ταυτόχρονα τα ίδια δεδομένα, οδηγώντας σε απρόβλεπτα αποτελέσματα. Το Synchronized έγινε το πρώτο και κύριο εργαλείο της Java για την αντιμετώπιση αυτού του προβλήματος, προσφέροντας απλή δηλωτική σύνταξη προσβάσιμη σε κάθε προγραμματιστή.
Ο μηχανισμός synchronized βασίζεται στην έννοια της οθόνης — ένα υψηλού επιπέδου πρωτόγονο συγχρονισμού ενσωματωμένο σε κάθε αντικείμενο Java. Η οθόνη συνδέεται με το αντικείμενο κατά την πρώτη χρήση ενός synchronized μπλοκ σε αυτό.
Κάθε αντικείμενο στη Java έχει μια σχετιζόμενη οθόνη. Όταν ένα νήμα εισέρχεται σε ένα synchronized μπλοκ, καταλαμβάνει την οθόνη του αντικειμένου. Εάν η οθόνη είναι ήδη κατειλημμένη από άλλο νήμα, το νήμα μπλοκάρεται μέχρι την απελευθέρωση. Σε bytecode, αυτό αντιστοιχεί στο ζεύγος εντολών monitorenter και monitorexit.
Η JVM βελτιστοποιεί το synchronized μέσω πολλών επιπέδων: biased locking (προκατειλημμένο κλείδωμα) για πρόσβαση ενός νήματος, lightweight locking (ελαφρύ κλείδωμα) σε χαμηλό ανταγωνισμό και heavyweight locking (βαρύ κλείδωμα) σε έντονο ανταγωνισμό με συμμετοχή του OS. Αυτά τα επίπεδα αυξάνουν την απόδοση χωρίς αλλαγή κώδικα.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Το Synchronized καθιερώνει τη σχέση happens-before: όλες οι ενέργειες σε ένα νήμα πριν από την έξοδο από ένα synchronized μπλοκ είναι ορατές σε ένα άλλο νήμα μετά την είσοδο στο μπλοκ που έχει συγχρονιστεί στο ίδιο αντικείμενο. Αυτό εγγυάται όχι μόνο τον αμοιβαίο αποκλεισμό, αλλά και τη συνέπεια δεδομένων για όλα τα νήματα.
Η Java προσφέρει δύο τρόπους εφαρμογής του synchronized: σε επίπεδο μεθόδου και σε επίπεδο μπλοκ. Η επιλογή μεταξύ τους επηρεάζει την απόδοση και την κοκκομετρία του συγχρονισμού.
Επισημαίνοντας μια μέθοδο με τον τροποποιητή synchronized, την συγχρονίζετε αυτόματα στο τρέχον στιγμιότυπο (για συνηθισμένη μέθοδο) ή στο αντικείμενο Class (για στατική μέθοδο). Αυτός είναι ο απλούστερος τρόπος διασφάλισης αμοιβαίου αποκλεισμού, αλλά συχνά είναι υπερβολικός αν η κρίσιμη ενότητα αποτελεί μόνο ένα μικρό μέρος της μεθόδου και ο υπόλοιπος κώδικας δεν απαιτεί συγχρονισμό.
Το synchronized μπλοκ παρέχει ακριβή έλεγχο: καθορίζετε το αντικείμενο-οθόνη και συγχρονίζετε μόνο το απαραίτητο τμήμα κώδικα, αφήνοντας το υπόλοιπο της μεθόδου εκτός κλειδώματος. Αυτό ελαχιστοποιεί τον χρόνο κατοχής της οθόνης και αυξάνει τη συνολική απόδοση της εφαρμογής σε πολυνηματικό περιβάλλον, καθώς άλλα νήματα μπορούν να εκτελούν παράλληλα μη σχετικό κώδικα χωρίς να περιμένουν την απελευθέρωση της οθόνης.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// κώδικας εκτός κρίσιμης ενότητας - χωρίς συγχρονισμό
prepareData()
synchronized (lock) {
// μόνο αυτό το μπλοκ προστατεύεται
updateSharedState()
}
// συνέχεια χωρίς κλείδωμα
cleanup()
}
}
| Κριτήριο | Synchronized μέθοδος | Synchronized μπλοκ |
|---|---|---|
| Οθόνη | this (στιγμιότυπο) ή Class | οποιοδήποτε αντικείμενο |
| Κοκκομετρία | ολόκληρη η μέθοδος | μόνο ο απαραίτητος κώδικας |
| Αναγνωσιμότητα | υψηλή | μέτρια |
| Απόδοση | χαμηλότερη για μεγάλη μέθοδο | υψηλότερη για μικρή κρίσιμη ενότητα |
Στην ανάπτυξη Android, το synchronized χρησιμοποιείται ευρέως για την προστασία SharedPreferences, πρόσβασης σε βάση δεδομένων και στοιχείων UI. Ωστόσο, η χρήση του στο κύριο νήμα δεν συνιστάται κατηγορηματικά λόγω κινδύνου παγώματος της διεπαφής.
Το SharedPreferences στο Android παρέχει βασική νηματική ασφάλεια, αλλά κατά την επεξεργασία από πολλαπλά νήματα μέσω Editor μπορεί να απαιτηθεί εξωτερικός συγχρονισμός. Ένα synchronized μπλοκ με ξεχωριστό αντικείμενο κλειδώματος εγγυάται τη συνέπεια των αλλαγών.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
Ο κύριος περιορισμός του synchronized στο Android — μπλοκάρισμα νήματος. Σε αντίθεση με τα coroutines με Mutex, το synchronized μπλοκάρει ολόκληρο το νήμα συστήματος. Στο κύριο νήμα, αυτό προκαλεί ANR. Στη σύγχρονη ανάπτυξη Android, συνιστάται η αντικατάσταση του synchronized με coroutines (suspend Mutex) ή ατομικούς τύπους (AtomicInteger).
Η σύγχρονη Java και Kotlin προσφέρουν πολλές εναλλακτικές για το synchronized, καθεμία από τις οποίες λύνει τις ίδιες εργασίες με λιγότερους περιορισμούς ή καλύτερη απόδοση.
Η διεπαφή Lock με υλοποιήσεις ReentrantLock και ReadWriteLock παρέχει χρονικά όρια, διακοπτόμενη αναμονή και πολλαπλές ουρές Condition. Είναι πιο ευέλικτη από το synchronized, αλλά απαιτεί ρητή απελευθέρωση στο finally, αυξάνοντας τον κίνδυνο σφάλματος σε περίπτωση ξεχασμένου unlock.
Οι κλάσεις AtomicInteger, AtomicLong, AtomicReference και άλλες χρησιμοποιούν Lock-Free αλγόριθμους βασισμένους σε CAS (Compare-And-Swap). Είναι σημαντικά ταχύτερες από το synchronized σε σενάρια με μέτριο ανταγωνισμό, καθώς δεν μπλοκάρουν νήματα, αλλά εκτελούν αισιόδοξες επαναληπτικές προσπάθειες και δεν απαιτούν εναλλαγή περιβάλλοντος από τον πυρήνα του OS.
Το ThreadLocal παρέχει μια εναλλακτική προσέγγιση: κάθε μεταβλητή ThreadLocal είναι απομονωμένη στο πλαίσιο ενός νήματος και δεν απαιτεί συγχρονισμό για ανάγνωση και εγγραφή. Αυτό εξαλείφει εντελώς την ανάγκη για synchronized για δεδομένα που δεν πρέπει να μοιράζονται μεταξύ νημάτων. Το ThreadLocal χρησιμοποιείται ενεργά σε frameworks (Spring, Hibernate) για αποθήκευση περιβάλλοντος συναλλαγών και συνεδριών.
Σε έργα Kotlin για Android, η εναλλακτική για το synchronized είναι το Mutex από kotlinx.coroutines. Δεν μπλοκάρει το νήμα του λειτουργικού συστήματος, αλλά αναστέλλει το coroutine μέχρι την απελευθέρωση του κλειδώματος — αυτό επιτρέπει την αποτελεσματική χρήση νημάτων από το pool και αποφυγή ANR κατά τη μακρά αναμονή για απελευθέρωση πόρου.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
Η απόδοση του synchronized έχει αλλάξει σημαντικά στις πρόσφατες εκδόσεις Java. Παλαιότερα θεωρούνταν „βαρύς„ μηχανισμός, αλλά οι σύγχρονες JVM έχουν εξαλείψει το μεγαλύτερο μέρος της επιβάρυνσης χάρη στις προηγμένες βελτιστοποιήσεις του μεταγλωττιστή JIT. Ας εξετάσουμε λεπτομερώς πώς η εικονική μηχανή επιταχύνει τον συγχρονισμένο κώδικα κατά τον χρόνο εκτέλεσης.
Ο μεταγλωττιστής JIT της JVM εφαρμόζει διάφορες βελτιστοποιήσεις: biased locking εξαλείφει τον συγχρονισμό αν το κλείδωμα καταλαμβάνεται πάντα από ένα νήμα; lock coarsening συνενώνει γειτονικά synchronized μπλοκ σε ένα; lock elimination αφαιρεί τον συγχρονισμό αν το αντικείμενο είναι προσβάσιμο μόνο από ένα νήμα. Αυτές οι βελτιστοποιήσεις καθιστούν το synchronized πρακτικά δωρεάν σε χαμηλό ανταγωνισμό.
Η JVM καθορίζει το επίπεδο ανταγωνισμού για κάθε αντικείμενο: σε απουσία ανταγωνισμού ενεργοποιείται το biased locking, με την εμφάνιση δεύτερου νήματος το κλείδωμα μεταβαίνει σε λειτουργία lightweight με spin-waiting, και μόνο σε παρατεταμένη αναμονή — σε heavyweight με mutex συστήματος. Αυτή η κλιμάκωση γίνεται αυτόματα και ο προγραμματιστής δεν χρειάζεται να επιλέξει χειροκίνητα στρατηγική.
Σε σύγχρονες μετρήσεις (Java 17+) το synchronized εμφανίζει απόδοση συγκρίσιμη με το ReentrantLock σε χαμηλό και μέτριο ανταγωνισμό. Σε υψηλό ανταγωνισμό, το Lock μπορεί να έχει πλεονέκτημα χάρη στην αποτελεσματικότερη ουρά αναμονής με υποστήριξη χρονικών ορίων και διακοπών. Για συστήματα υψηλού φόρτου όπου ο ανταγωνισμός είναι σταθερός, το ReentrantLock σε λειτουργία fair παρέχει πιο προβλέψιμη συμπεριφορά.
Οι ατομικές κλάσεις (AtomicInteger, AtomicReference) παραμένουν οι ταχύτερες για απλούς μετρητές και σημαίες χάρη στην υλοποίηση Lock-Free σε CAS. Δεν μπλοκάρουν καθόλου τα νήματα — σε περίπτωση σύγκρουσης, η λειτουργία απλώς επαναλαμβάνεται σε έναν βρόχο. Αυτό δίνει βελτίωση απόδοσης 3-5 φορές σε σύγκριση με το synchronized σε λειτουργίες αύξησης μετρητή σε 4-8 νήματα.
Συχνές ερωτήσεις
Synchronized παρέχει τόσο αμοιβαίο αποκλεισμό όσο και ορατότητα. Το Volatile εγγυάται μόνο την ορατότητα αλλαγών — η εγγραφή σε μια volatile μεταβλητή είναι ορατή σε όλα τα νήματα, αλλά δεν αποτρέπει την ταυτόχρονη τροποποίηση, δηλαδή δεν προστατεύει από συνθήκη ανταγωνισμού.
Ναι, deadlock είναι πιθανός σε ένθετο συγχρονισμό με διαφορετική σειρά οθονών. Για παράδειγμα, ένα νήμα καλεί synchronized(a) { synchronized(b) }, και ένα άλλο — synchronized(b) { synchronized(a) }. Αποφύγετε ένθετα synchronized μπλοκ ή καθορίστε ενιαία σειρά οθονών.
Οθόνη είναι ένας μηχανισμός συγχρονισμού που σχετίζεται με κάθε αντικείμενο Java. Εγγυάται ότι μόνο ένα νήμα εκτελεί synchronized κώδικα σε αυτό το αντικείμενο. Η οθόνη περιλαμβάνει κλείδωμα, ουρά αναμονής και σύνολο νημάτων που περιμένουν ειδοποίηση μέσω wait/notify.
Σε σύγχρονες εκδόσεις Java (17+) το synchronized δεν υστερεί έναντι του Lock σε απόδοση χάρη στις βελτιστοποιήσεις JIT (biased locking, lock coarsening). Το Lock προτιμάται όχι λόγω ταχύτητας, αλλά λόγω πρόσθετων δυνατοτήτων: χρονικά όρια, διακοπτόμενη αναμονή και πολλαπλές Condition.
Στατική synchronized μέθοδος χρησιμοποιεί την οθόνη του αντικειμένου Class αυτής της κλάσης, όχι του στιγμιοτύπου. Αυτό σημαίνει ότι ο συγχρονισμός καλύπτει όλα τα στιγμιότυπα της κλάσης. Οι μη στατικές και στατικές synchronized μέθοδοι χρησιμοποιούν διαφορετικές οθόνες και δεν μπλοκάρουν η μία την άλλη.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης