Synchronized egy beépített szinkronizációs mechanizmus a Java nyelvben, amely kizárólagos hozzáférést biztosít a kód kritikus szekcióihoz. A Oracle, 2024 szerint a synchronized módosító garantálja, hogy csak egy szál hajthatja végre a megjelölt metódust vagy blokkot egy adott pillanatban. Ez a mechanizmus monitorokon alapul — az operációs rendszerek alapvető koncepcióján, amely biztosítja a többszálú alkalmazások helyes működését a komplexitás minden szintjén.
Főbb pontok
Synchronized egy kulcsszó Java-ban, amely garantálja, hogy egyszerre csak egy szál hajtja végre a védett kódrészt, megakadályozva az adatok sérülését párhuzamos hozzáférés esetén. A Java első verziójában jelent meg, és a legegyszerűbb módja maradt a szálbiztonság biztosításának minden szintű fejlesztő számára.
A synchronized módosító két feladatot old meg: kölcsönös kizárás (mutual exclusion) és a változtatások láthatósága (visibility). Amikor egy szál elhagy egy synchronized blokkot, az összes változtatás garantáltan látható más szálak számára, amelyek ugyanazon az objektumon szinkronizált blokkba lépnek.
A synchronized alkalmazható a teljes metódusra vagy egy tetszőleges kódblokkra a monitor-objektum megadásával. Mindkét esetben a JVM a monitorenter és monitorexit utasításokat illeszti be bájtkód szinten.
A többszálú alkalmazásokban szinkronizáció nélkül versenyhelyzet (race condition) lép fel — amikor két szál egyidejűleg módosítja ugyanazokat az adatokat, kiszámíthatatlan eredményekhez vezetve. A synchronized lett a Java első és fő eszköze a probléma leküzdésére, egyszerű deklaratív szintaxist biztosítva, amely minden fejlesztő számára elérhető.
A synchronized mechanizmusa a monitor koncepcióján alapul — egy magas szintű szinkronizációs primitíven, amely minden Java objektumba be van építve. A monitor az objektumhoz kapcsolódik a synchronized blokk első használatakor.
Minden Java objektumnak van egy kapcsolódó monitorja. Amikor egy szál belép egy synchronized blokkba, átveszi az objektum monitorját. Ha a monitor már egy másik szál által foglalt, a szál blokkolva van a felszabadulásig. Bájtkód szinten ennek a monitorenter és monitorexit utasításpár felel meg.
A JVM több szinten optimalizálja a synchronized-t: biased locking (elforgatott zárolás) egyszálú hozzáféréshez, lightweight locking (könnyű zárolás) gyenge versengésnél és heavyweight locking (nehéz zárolás) intenzív versengésnél az OS részvételével. Ezek a szintek növelik a teljesítményt a kód megváltoztatása nélkül.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
A synchronized happens-before kapcsolatot hoz létre: minden művelet egy szálban a synchronized blokk elhagyása előtt látható egy másik szál számára az ugyanazon az objektumon szinkronizált blokkba való belépés után. Ez nemcsak a kölcsönös kizárást, hanem az adatok konzisztenciáját is garantálja az összes szál számára.
A Java két módot kínál a synchronized alkalmazására: metódus szinten és blokk szinten. A választás közöttük befolyásolja a teljesítményt és a szinkronizáció granularitását.
Egy metódus synchronized módosítóval való megjelölésével automatikusan szinkronizálja azt az aktuális példányon (hétköznapi metódus esetén) vagy a Class objektumon (statikus metódus esetén). Ez a legegyszerűbb módja a kölcsönös kizárás biztosításának, de gyakran túlzó, ha a kritikus szekció csak a metódus egy kis részét képezi, és a kód többi része nem igényel szinkronizációt.
A synchronized blokk pontos vezérlést biztosít: megadja a monitor-objektumot, és csak a szükséges kódrészt szinkronizálja, a metódus többi részét a zároláson kívül hagyva. Ez minimalizálja a monitor birtoklásának idejét és növeli az alkalmazás általános teljesítményét többszálú környezetben, mivel más szálak párhuzamosan végrehajthatnak nem kapcsolódó kódot anélkül, hogy a monitor felszabadulására várnának.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// kód a kritikus szakaszon kívül - szinkronizálás nélkül
prepareData()
synchronized (lock) {
// csak ez a blokk védett
updateSharedState()
}
// folytatás zárolás nélkül
cleanup()
}
}
| Kritérium | Synchronized metódus | Synchronized blokk |
|---|---|---|
| Monitor | this (példány) vagy Class | bármely objektum |
| Granularitás | teljes metódus | csak szükséges kód |
| Olvashatóság | magas | közepes |
| Teljesítmény | alacsonyabb nagy metódusnál | magasabb kis kritikus szekciónál |
Android fejlesztésben a synchronized széles körben használatos a SharedPreferences, adatbázis-hozzáférés és UI komponensek védelmére. Használata a főszálon azonban kifejezetten nem ajánlott a felület lefagyásának kockázata miatt.
A SharedPreferences Android-ban alapvető szálbiztonságot nyújt, de több szál általi szerkesztéskor Editor-on keresztül külső szinkronizációra lehet szükség. A synchronized blokk külön zárolási objektummal garantálja a változtatások konzisztenciáját.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
A synchronized fő korlátozása Android-on — szálblokkolás. A Mutex-szel ellátott korutinokkal ellentétben a synchronized blokkolja a teljes rendszerszálat. A főszálon ez ANR-t okoz. Modern Android fejlesztésben ajánlott a synchronized helyettesítése korutinokkal (suspend Mutex) vagy atomi típusokkal (AtomicInteger).
A modern Java és Kotlin több alternatívát kínál a synchronized helyett, amelyek mindegyike ugyanazokat a feladatokat oldja meg kevesebb korlátozással vagy jobb teljesítménnyel.
A Lock interfész a ReentrantLock és ReadWriteLock implementációkkal időtúllépéseket, megszakítható várakozást és több Condition várakozási sort biztosít. Rugalmasabb, mint a synchronized, de explicit felszabadítást igényel a finally blokkban, ami növeli a hiba kockázatát elfelejtett unlock esetén.
Az AtomicInteger, AtomicLong, AtomicReference és más osztályok CAS (Compare-And-Swap) alapú Lock-Free algoritmusokat használnak. Jelentősen gyorsabbak, mint a synchronized mérsékelt versengésű forgatókönyvekben, mivel nem blokkolják a szálakat, hanem optimista újrapróbálkozásokat végeznek, és nem igényelnek kontextusváltást az OS kernel által.
A ThreadLocal alternatív megközelítést kínál: minden ThreadLocal változó egy szálon belül van elkülönítve, és nem igényel szinkronizációt olvasáshoz és íráshoz. Ez teljesen kiküszöböli a synchronized szükségességét azon adatok esetében, amelyeket nem kell megosztani a szálak között. A ThreadLocal aktívan használatos keretrendszerekben (Spring, Hibernate) tranzakciós kontextus és munkamenetek tárolására.
Androidhoz készült Kotlin projektekben a synchronized alternatívája a Mutex a kotlinx.coroutines-ból. Ez nem blokkolja az operációs rendszer szálát, hanem felfüggeszti a korutint a zárolás felszabadulásáig — ez lehetővé teszi a pool szálak hatékony használatát és az ANR elkerülését az erőforrás felszabadulására való hosszú várakozáskor.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
A synchronized teljesítménye jelentősen megváltozott a Java legújabb verzióiban. Korábban „nehéz„ mechanizmusnak tartották, de a modern JVM-ek a JIT fordító fejlett optimalizálásainak köszönhetően megszüntették a többletterhelés nagy részét. Vizsgáljuk meg részletesen, hogyan gyorsítja fel a virtuális gép a szinkronizált kódot futásidőben.
A JVM JIT fordítója több optimalizálást alkalmaz: biased locking megszünteti a szinkronizációt, ha a zárolást mindig egy szál veszi át; lock coarsening egyesíti a szomszédos synchronized blokkokat egybe; lock elimination eltávolítja a szinkronizációt, ha az objektum csak egy szál számára elérhető. Ezek az optimalizálások a synchronized-t gyakorlatilag ingyenessé teszik alacsony versengés esetén.
A JVM meghatározza a versengés szintjét minden objektumhoz: versengés hiányában a biased locking aktiválódik, a második szál megjelenésekor a zárolás lightweight módba vált spin-waitinggel, és csak hosszú várakozáskor — heavyweight-be rendszermutex-szel. Ez az eszkaláció automatikusan történik, és a fejlesztőnek nem kell manuálisan stratégiát választania.
A modern benchmarkokban (Java 17+) a synchronized a ReentrantLock-hoz hasonló teljesítményt mutat alacsony és mérsékelt versengés esetén. Magas versengésnél a Lock előnyösebb lehet a hatékonyabb várakozási sor miatt, időtúllépések és megszakítások támogatásával. Magas terhelésű rendszerekhez, ahol a versengés állandó, a ReentrantLock fair módban kiszámíthatóbb viselkedést biztosít.
Az atomi osztályok (AtomicInteger, AtomicReference) maradnak a leggyorsabbak egyszerű számlálók és jelzők számára a CAS-on alapuló Lock-Free implementációnak köszönhetően. Egyáltalán nem blokkolják a szálakat — konfliktus esetén a művelet egyszerűen megismétlődik egy ciklusban. Ez 3-5-szörös teljesítménynövekedést ad a synchronized-hez képest számláló növelési műveleteknél 4-8 szálon.
Gyakran ismételt kérdések
Synchronized mind a kölcsönös kizárást, mind a láthatóságot biztosítja. A Volatile csak a változtatások láthatóságát garantálja — a volatile változóba írás látható az összes szál számára, de nem akadályozza meg az egyidejű módosítást, vagyis nem véd a versenyhelyzet ellen.
Igen, deadlock lehetséges beágyazott szinkronizációnál eltérő monitor sorrend esetén. Például az egyik szál synchronized(a) { synchronized(b) } hívást végez, a másik pedig — synchronized(b) { synchronized(a) }. Kerülje a beágyazott synchronized blokkokat, vagy rögzítsen egységes monitor sorrendet.
Monitor egy szinkronizációs mechanizmus, amely minden Java objektumhoz kapcsolódik. Garantálja, hogy csak egy szál hajt végre synchronized kódot az adott objektumon. A monitor magában foglalja a zárolást, a várakozási sort és a wait/notify-n keresztül értesítésre váró szálak készletét.
A Java modern verzióiban (17+) a synchronized nem marad el a Lock mögött teljesítményben a JIT optimalizálásoknak (biased locking, lock coarsening) köszönhetően. A Lock nem a sebesség miatt előnyösebb, hanem a további képességek miatt: időtúllépések, megszakítható várakozás és több Condition.
Statikus synchronized metódus az osztály Class objektumának monitorját használja, nem a példányét. Ez azt jelenti, hogy a szinkronizáció az osztály összes példányára kiterjed. A nem statikus és statikus synchronized metódusok különböző monitorokat használnak, és nem blokkolják egymást.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is