Synchronized è un meccanismo di sincronizzazione integrato nel linguaggio Java che fornisce accesso esclusivo a sezioni critiche di codice. Secondo Oracle, 2024, il modificatore synchronized garantisce che un solo thread può eseguire il metodo o blocco marcato in un momento specifico. Questo meccanismo si basa sui monitor — un concetto fondamentale dei sistemi operativi che assicura il corretto funzionamento di applicazioni multithread di tutti i livelli di complessità.
Punti chiave
Synchronized è una parola chiave in Java che garantisce che un solo thread esegua una sezione protetta di codice alla volta, prevenendo la corruzione dei dati durante l'accesso concorrente. È apparso nella prima versione di Java e rimane il modo più semplice per garantire la thread safety per sviluppatori di ogni livello.
Il modificatore synchronized risolve due compiti: mutua esclusione e visibilità delle modifiche. Quando un thread esce da un blocco synchronized, tutte le modifiche sono garantite visibili ad altri thread che entrano in un blocco sincronizzato sullo stesso oggetto.
Synchronized può essere applicato a un intero metodo o a un blocco di codice arbitrario specificando un oggetto monitor. In entrambi i casi, la JVM inserisce le istruzioni monitorenter e monitorexit a livello di bytecode.
Nelle applicazioni multithread senza sincronizzazione, si verifica una condizione di competizione (race condition) quando due thread modificano simultaneamente gli stessi dati, portando a risultati imprevedibili. Synchronized è diventato il primo e principale strumento di Java per combattere questo problema, fornendo una sintassi dichiarativa semplice accessibile a qualsiasi sviluppatore.
Il meccanismo synchronized si basa sul concetto di monitor — un primitivo di sincronizzazione di alto livello incorporato in ogni oggetto Java. Il monitor viene associato a un oggetto quando si usa per la prima volta un blocco synchronized su di esso.
Ogni oggetto in Java ha un monitor associato. Quando un thread entra in un blocco synchronized, acquisisce il monitor dell'oggetto. Se il monitor è già occupato da un altro thread, il thread si blocca fino al rilascio. Nel bytecode, ciò corrisponde alla coppia di istruzioni monitorenter e monitorexit.
La JVM ottimizza synchronized attraverso diversi livelli: biased locking (blocco polarizzato) per accesso a thread singolo, lightweight locking (blocco leggero) per bassa contesa, e heavyweight locking (blocco pesante) per contesa intensa con coinvolgimento del SO. Questi livelli migliorano le prestazioni senza modificare il codice.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized stabilisce una relazione happens-before: tutte le azioni in un thread prima di uscire da un blocco synchronized sono visibili a un altro thread dopo essere entrato in un blocco sincronizzato sullo stesso oggetto. Questo garantisce non solo mutua esclusione ma anche consistenza dei dati per tutti i thread.
Java offre due modi per applicare synchronized: a livello di metodo e a livello di blocco. La scelta tra di essi influisce sulle prestazioni e sulla granularità della sincronizzazione.
Marcare un metodo con il modificatore synchronized lo sincronizza automaticamente sull'istanza corrente (per i metodi di istanza) o sull'oggetto Class (per i metodi statici). È il modo più semplice per garantire la mutua esclusione, ma è spesso eccessivo se la sezione critica costituisce solo una piccola parte del metodo e il resto del codice non richiede sincronizzazione.
Un blocco synchronized offre un controllo preciso: si specifica l'oggetto monitor e si sincronizza solo la sezione di codice necessaria, lasciando il resto del metodo fuori dal blocco. Questo minimizza il tempo di possesso del monitor e migliora le prestazioni complessive dell'applicazione in un ambiente multithread, poiché altri thread possono eseguire codice non correlato in parallelo senza attendere il rilascio del monitor.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// codice fuori dalla sezione critica - senza sincronizzazione
prepareData()
synchronized (lock) {
// solo questo blocco è protetto
updateSharedState()
}
// continuazione senza blocco
cleanup()
}
}
| Criterio | Metodo synchronized | Blocco synchronized |
|---|---|---|
| Monitor | this (istanza) o Class | qualsiasi oggetto |
| Granularità | intero metodo | solo il codice necessario |
| Leggibilità | alta | media |
| Prestazioni | inferiori per metodi grandi | superiori per sezioni critiche piccole |
Nello sviluppo Android, synchronized è ampiamente utilizzato per proteggere SharedPreferences, l'accesso al database e i componenti dell'interfaccia utente. Tuttavia, il suo utilizzo sul thread principale è fortemente sconsigliato a causa del rischio di congelamento dell'interfaccia.
SharedPreferences in Android fornisce una sicurezza di base tra i thread, ma durante la modifica da più thread tramite Editor, potrebbe essere necessaria una sincronizzazione esterna. Un blocco synchronized con un oggetto di blocco separato garantisce la consistenza delle modifiche.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
La limitazione principale di synchronized su Android è il blocco del thread. A differenza delle coroutine con Mutex, synchronized blocca completamente il thread di sistema. Sul thread principale, ciò causa ANR. Nello sviluppo Android moderno, si consiglia di sostituire synchronized con coroutine (suspend Mutex) o tipi atomici (AtomicInteger).
Java e Kotlin moderni offrono diverse alternative a synchronized, ognuna delle quali risolve gli stessi problemi con meno limitazioni o migliori prestazioni.
L'interfaccia Lock con le implementazioni ReentrantLock e ReadWriteLock fornisce timeout, attesa interruptibile e multiple code Condition. È più flessibile di synchronized ma richiede il rilascio esplicito in finally, aumentando il rischio di errore se si dimentica l'unlock.
AtomicInteger, AtomicLong, AtomicReference e altre classi utilizzano algoritmi Lock-Free basati su CAS (Compare-And-Swap). Sono significativamente più veloci di synchronized in scenari di contesa moderata perché non bloccano i thread ma eseguono tentativi ottimistici senza richiedere il cambio di contesto del kernel del SO.
ThreadLocal fornisce un approccio alternativo: ogni variabile ThreadLocal è isolata all'interno di un singolo thread e non richiede sincronizzazione per lettura e scrittura. Ciò elimina completamente la necessità di synchronized per i dati che non devono essere condivisi tra i thread. ThreadLocal è attivamente utilizzato nei framework (Spring, Hibernate) per memorizzare il contesto delle transazioni e delle sessioni.
Nei progetti Kotlin per Android, un'alternativa a synchronized è Mutex di kotlinx.coroutines. Non blocca il thread del sistema operativo ma sospende la coroutine fino al rilascio del blocco — ciò consente un uso efficiente dei thread del pool e previene ANR durante le lunghe attese per il rilascio della risorsa.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
Le prestazioni di synchronized sono cambiate significativamente nelle versioni recenti di Java. Prima era considerato un meccanismo “pesante”, ma le JVM moderne hanno eliminato la maggior parte dell'overhead grazie alle ottimizzazioni avanzate del compilatore JIT. Vediamo in dettaglio come la macchina virtuale accelera il codice sincronizzato in fase di esecuzione.
Il compilatore JIT della JVM applica diverse ottimizzazioni: biased locking elimina la sincronizzazione se il blocco è sempre acquisito dallo stesso thread; lock coarsening unisce blocchi synchronized adiacenti in uno solo; lock elimination rimuove la sincronizzazione se l'oggetto è accessibile solo da un thread. Queste ottimizzazioni rendono synchronized praticamente gratuito in caso di bassa contesa.
La JVM determina il livello di contesa per ogni oggetto: quando non c'è contesa, viene attivato il biased locking; quando appare un secondo thread, il blocco passa alla modalità leggera con attesa spin; e solo durante l'attesa prolungata scala a pesante con un mutex di sistema. Questa escalation avviene automaticamente e lo sviluppatore non deve scegliere manualmente una strategia.
Nei benchmark moderni (Java 17+), synchronized mostra prestazioni comparabili a ReentrantLock con contesa bassa e moderata. Con contesa elevata, Lock può avere un vantaggio grazie a una coda di attesa più efficiente con supporto di timeout e interrupt. Per i sistemi ad alto carico dove la contesa è costante, ReentrantLock con modalità fair offre un comportamento più prevedibile.
Le classi atomiche (AtomicInteger, AtomicReference) rimangono le più veloci per contatori e flag semplici grazie all'implementazione Lock-Free basata su CAS. Non bloccano affatto i thread: in caso di conflitto, l'operazione si ripete semplicemente in un ciclo. Ciò fornisce un guadagno di prestazioni da 3 a 5 volte rispetto a synchronized sulle operazioni di incremento del contatore con 4-8 thread.
Domande frequenti
Synchronized fornisce sia mutua esclusione che visibilità. Volatile garantisce solo la visibilità delle modifiche — scrivere in una variabile volatile è visibile a tutti i thread ma non impedisce la modifica simultanea, quindi non protegge dalle condizioni di competizione.
Sì, un deadlock è possibile con sincronizzazione annidata usando ordini di monitor diversi. Ad esempio, un thread chiama synchronized(a) { synchronized(b) }, mentre un altro chiama synchronized(b) { synchronized(a) }. Evitate blocchi synchronized annidati o fissate un ordine di monitor consistente.
Un monitor è un meccanismo di sincronizzazione associato a ogni oggetto Java. Garantisce che un solo thread esegua codice synchronized su quell'oggetto. Il monitor include un blocco, una coda di attesa e un pool di thread in attesa di notifica tramite wait/notify.
Nelle versioni moderne di Java (17+), synchronized non è inferiore a Lock in termini di prestazioni grazie alle ottimizzazioni JIT (biased locking, lock coarsening). Lock è preferito non per la velocità ma per le funzionalità aggiuntive: timeout, attesa interruptibile e multiple code Condition.
Un metodo statico synchronized utilizza il monitor dell'oggetto Class della classe specificata, non dell'istanza. Ciò significa che la sincronizzazione si applica a tutte le istanze della classe. I metodi non statici e statici synchronized utilizzano monitor diversi e non si bloccano a vicenda.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche