Synchronized is een ingebouwd synchronisatiemechanisme in de programmeertaal Java, dat exclusieve toegang tot kritieke codegedeelten biedt. Volgens Oracle, 2024 garandeert de modifier synchronized dat slechts één thread de gemarkeerde methode of blok op een bepaald moment kan uitvoeren. Dit mechanisme is gebaseerd op monitoren — een fundamenteel concept van besturingssystemen dat de correcte werking van multithreadingtoepassingen op alle niveaus van complexiteit waarborgt.
Belangrijkste punten
Synchronized is een sleutelwoord in Java dat garandeert dat slechts één thread tegelijk het beschermde codegedeelte uitvoert, waardoor beschadiging van gegevens bij parallelle toegang wordt voorkomen. Het verscheen in de eerste versie van Java en blijft de eenvoudigste manier om threadveiligheid te waarborgen voor ontwikkelaars van elk niveau.
De modifier synchronized lost twee taken op: wederzijdse uitsluiting (mutual exclusion) en zichtbaarheid van wijzigingen (visibility). Wanneer een thread een synchronized blok verlaat, zijn alle wijzigingen gegarandeerd zichtbaar voor andere threads die het blok betreden dat op hetzelfde object is gesynchroniseerd.
Synchronized kan worden toegepast op de gehele methode of op een willekeurig codeblok met opgave van het monitor-object. In beide gevallen plaatst de JVM de instructies monitorenter en monitorexit op bytecode-niveau.
In multithreadingtoepassingen zonder synchronisatie treedt race condition op — wanneer twee threads tegelijkertijd dezelfde gegevens wijzigen, leidt dit tot onvoorspelbare resultaten. Synchronized werd het eerste en belangrijkste hulpmiddel van Java om dit probleem te bestrijden, met een eenvoudige declaratieve syntaxis die voor elke ontwikkelaar toegankelijk is.
Het mechanisme van synchronized is gebaseerd op het concept van een monitor — een hoogwaardig synchronisatieprimitief dat in elk Java-object is ingebouwd. De monitor wordt aan het object gekoppeld bij het eerste gebruik van een synchronized blok erop.
Elk object in Java heeft een bijbehorende monitor. Wanneer een thread een synchronized blok betreedt, neemt deze de monitor van het object over. Als de monitor al door een andere thread is bezet, wordt de thread geblokkeerd totdat deze vrijkomt. In bytecode komt dit overeen met het paar instructies monitorenter en monitorexit.
De JVM optimaliseert synchronized via verschillende niveaus: biased locking (gekeurde vergrendeling) voor single-thread toegang, lightweight locking (lichte vergrendeling) bij lage concurrentie en heavyweight locking (zware vergrendeling) bij intense concurrentie met deelname van het besturingssysteem. Deze niveaus verhogen de prestaties zonder de code te wijzigen.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized stelt de happens-before-relatie in: alle acties in een thread vóór het verlaten van een synchronized blok zijn zichtbaar voor een andere thread na het betreden van het blok dat op hetzelfde object is gesynchroniseerd. Dit garandeert niet alleen wederzijdse uitsluiting, maar ook de consistentie van gegevens voor alle threads.
Java biedt twee manieren om synchronized toe te passen: op methodeniveau en op blokniveau. De keuze tussen beide beïnvloedt de prestaties en granulariteit van de synchronisatie.
Door een methode te markeren met de modifier synchronized, synchroniseert u deze automatisch op de huidige instantie (voor een gewone methode) of op het Class-object (voor een statische methode). Dit is de eenvoudigste manier om wederzijdse uitsluiting te garanderen, maar is vaak overbodig als het kritieke gedeelte slechts een klein deel van de methode vormt en de rest van de code geen synchronisatie vereist.
Het synchronized blok biedt nauwkeurige controle: u geeft monitor-object op en synchroniseert alleen het benodigde codegedeelte, terwijl de rest van de methode buiten de vergrendeling blijft. Dit minimaliseert de tijd dat de monitor wordt vastgehouden en verhoogt de algehele prestaties van de toepassing in een multithreadingomgeving, omdat andere threads parallel niet-gerelateerde code kunnen uitvoeren zonder te wachten op het vrijkomen van de monitor.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// code buiten kritieke sectie - zonder synchronisatie
prepareData()
synchronized (lock) {
// alleen dit blok is beschermd
updateSharedState()
}
// verder zonder vergrendeling
cleanup()
}
}
| Criterium | Synchronized methode | Synchronized blok |
|---|---|---|
| Monitor | this (instantie) of Class | elk willekeurig object |
| Granulariteit | gehele methode | alleen benodigde code |
| Leesbaarheid | hoog | gemiddeld |
| Prestaties | lager bij grote methode | hoger bij kleine kritieke sectie |
In Android-ontwikkeling wordt synchronized veel gebruikt voor de bescherming van SharedPreferences, databasetoegang en UI-componenten. Het gebruik ervan op de hoofdthread wordt echter ten strengste afgeraden vanwege het risico op vastlopen van de interface.
SharedPreferences in Android biedt basis-threadveiligheid, maar bij bewerking door meerdere threads via Editor kan externe synchronisatie nodig zijn. Een synchronized blok met een apart vergrendelingsobject garandeert de consistentie van wijzigingen.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
De belangrijkste beperking van synchronized op Android — threadblokkering. In tegenstelling tot coroutines met Mutex, blokkeert synchronized de volledige systeemthread. Op de hoofdthread veroorzaakt dit ANR. In moderne Android-ontwikkeling wordt aanbevolen synchronized te vervangen door coroutines (suspend Mutex) of atomaire typen (AtomicInteger).
Modern Java en Kotlin bieden verschillende alternatieven voor synchronized, die elk dezelfde taken oplossen met minder beperkingen of betere prestaties.
De interface Lock met implementaties ReentrantLock en ReadWriteLock biedt time-outs, onderbreekbaar wachten en meerdere Condition-wachtrijen. Het is flexibeler dan synchronized, maar vereist expliciete vrijgave in finally, wat het risico op fouten bij een vergeten unlock vergroot.
De klassen AtomicInteger, AtomicLong, AtomicReference en andere gebruiken Lock-Free algoritmen op basis van CAS (Compare-And-Swap). Ze zijn aanzienlijk sneller dan synchronized in scenario's met matige concurrentie, omdat ze threads niet blokkeren, maar optimistische herhaalde pogingen uitvoeren en geen contextomschakeling door de OS-kernel vereisen.
ThreadLocal biedt een alternatieve benadering: elke ThreadLocal-variabele is geïsoleerd binnen één thread en vereist geen synchronisatie voor lezen en schrijven. Dit elimineert volledig de noodzaak voor synchronized voor gegevens die niet tussen threads moeten worden gedeeld. ThreadLocal wordt actief gebruikt in frameworks (Spring, Hibernate) voor het opslaan van transactiecontext en sessies.
In Kotlin-projecten voor Android is het alternatief voor synchronized Mutex uit kotlinx.coroutines. Het blokkeert de besturingssysteemthread niet, maar onderbreekt de coroutine tot het vrijgeven van de vergrendeling — dit maakt efficiënt gebruik van poolthreads mogelijk en voorkomt ANR bij lang wachten op het vrijkomen van een bron.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
De prestaties van synchronized zijn aanzienlijk veranderd in de recente versies van Java. Vroeger werd het beschouwd als een „zwaar„ mechanisme, maar moderne JVM's hebben de meeste overhead geëlimineerd dankzij geavanceerde optimalisaties van de JIT-compiler. Laten we in detail bekijken hoe de virtuele machine gesynchroniseerde code tijdens runtime versnelt.
De JIT-compiler van de JVM past verschillende optimalisaties toe: biased locking elimineert synchronisatie als de vergrendeling altijd door één thread wordt overgenomen; lock coarsening voegt aangrenzende synchronized blokken samen tot één; lock elimination verwijdert synchronisatie als het object slechts voor één thread toegankelijk is. Deze optimalisaties maken synchronized praktisch gratis bij lage concurrentie.
De JVM bepaalt het concurrentieniveau voor elk object: bij afwezigheid van concurrentie wordt biased locking ingeschakeld, bij het verschijnen van een tweede thread gaat de vergrendeling over in de lightweight-modus met spin-waiting, en pas bij lang wachten — in heavyweight met systeem-mutex. Deze escalatie vindt automatisch plaats en de ontwikkelaar hoeft niet handmatig een strategie te kiezen.
In moderne benchmarks (Java 17+) vertoont synchronized prestaties vergelijkbaar met ReentrantLock bij lage en matige concurrentie. Bij hoge concurrentie kan Lock een voordeel hebben dankzij een efficiëntere wachtrij met ondersteuning voor time-outs en onderbrekingen. Voor systemen met hoge belasting waar concurrentie constant is, biedt ReentrantLock in fair-modus voorspelbaarder gedrag.
Atomaire klassen (AtomicInteger, AtomicReference) blijven het snelst voor eenvoudige tellers en vlaggen dankzij Lock-Free implementatie op CAS. Ze blokkeren threads helemaal niet — bij een conflict wordt de bewerking eenvoudig in een lus herhaald. Dit geeft een prestatieverbetering van 3-5 keer in vergelijking met synchronized bij tellerophogingen op 4-8 threads.
Veelgestelde vragen
Synchronized biedt zowel wederzijdse uitsluiting als zichtbaarheid. Volatile garandeert alleen de zichtbaarheid van wijzigingen — schrijven naar een volatile-variabele is zichtbaar voor alle threads, maar voorkomt niet gelijktijdige wijziging, dat wil zeggen, het beschermt niet tegen race conditions.
Ja, deadlock is mogelijk bij geneste synchronisatie met verschillende volgorde van monitoren. Bijvoorbeeld, de ene thread roept synchronized(a) { synchronized(b) } aan en de andere — synchronized(b) { synchronized(a) }. Vermijd geneste synchronized blokken of bepaal een vaste volgorde van monitoren.
Een monitor is een synchronisatiemechanisme dat aan elk Java-object is gekoppeld. Het garandeert dat slechts één thread synchronized code op dat object uitvoert. De monitor omvat de vergrendeling, een wachtrij en een pool van threads die wachten op melding via wait/notify.
In moderne versies van Java (17+) staat synchronized niet achter bij Lock qua prestaties dankzij JIT-optimalisaties (biased locking, lock coarsening). Lock heeft de voorkeur niet vanwege snelheid, maar vanwege extra mogelijkheden: time-outs, onderbreekbaar wachten en meerdere Condition.
Een statische synchronized methode gebruikt de monitor van het Class-object van die klasse, niet van de instantie. Dit betekent dat de synchronisatie alle instanties van de klasse omvat. Niet-statische en statische synchronized methoden gebruiken verschillende monitoren en blokkeren elkaar niet.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook