Synchronized est un mécanisme de synchronisation intégré au langage Java qui fournit un accès exclusif aux sections critiques de code. Selon Oracle, 2024, le modificateur synchronized garantit qu'un seul thread peut exécuter la méthode ou le bloc marqué à un moment précis. Ce mécanisme repose sur les moniteurs, un concept fondamental des systèmes d'exploitation qui assure le bon fonctionnement des applications multithread de tous niveaux de complexité.
Points clés
Synchronized est un mot-clé en Java qui garantit qu'un seul thread exécute une section protégée de code à un moment donné, empêchant la corruption de données lors de l'accès concurrent. Il est apparu dans la première version de Java et reste le moyen le plus simple d'assurer la thread safety pour les développeurs de tout niveau.
Le modificateur synchronized résout deux tâches : l'exclusion mutuelle et la visibilité des modifications. Lorsqu'un thread sort d'un bloc synchronized, toutes les modifications sont garanties visibles pour les autres threads qui entrent dans un bloc synchronisé sur le même objet.
Synchronized peut être appliqué à une méthode entière ou à un bloc de code arbitraire en spécifiant un objet moniteur. Dans les deux cas, la JVM insère les instructions monitorenter et monitorexit au niveau du bytecode.
Dans les applications multithread sans synchronisation, une condition de course (race condition) se produit lorsque deux threads modifient simultanément les mêmes données, entraînant des résultats imprévisibles. Synchronized est devenu le premier et principal outil de Java pour lutter contre ce problème, offrant une syntaxe déclarative simple accessible à tout développeur.
Le mécanisme synchronized est basé sur le concept de moniteur, une primitive de synchronisation de haut niveau intégrée dans chaque objet Java. Le moniteur est associé à un objet lors de la première utilisation d'un bloc synchronized sur celui-ci.
Chaque objet en Java a un moniteur associé. Lorsqu'un thread entre dans un bloc synchronized, il acquiert le moniteur de l'objet. Si le moniteur est déjà occupé par un autre thread, le thread se bloque jusqu'à sa libération. En bytecode, cela correspond à la paire d'instructions monitorenter et monitorexit.
La JVM optimise synchronized à travers plusieurs niveaux : biased locking (verrouillage biaisé) pour l'accès mono-thread, lightweight locking (verrouillage léger) pour une faible contention, et heavyweight locking (verrouillage lourd) pour une contention intense avec participation du système d'exploitation. Ces niveaux améliorent les performances sans modifier le code.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized établit une relation happens-before : toutes les actions dans un thread avant de sortir d'un bloc synchronized sont visibles pour un autre thread après être entré dans un bloc synchronisé sur le même objet. Cela garantit non seulement l'exclusion mutuelle mais aussi la cohérence des données pour tous les threads.
Java offre deux façons d'appliquer synchronized : au niveau de la méthode et au niveau du bloc. Le choix entre eux affecte les performances et la granularité de la synchronisation.
Marquer une méthode avec le modificateur synchronized la synchronise automatiquement sur l'instance courante (pour les méthodes d'instance) ou sur l'objet Class (pour les méthodes statiques). C'est le moyen le plus simple d'assurer l'exclusion mutuelle, mais il est souvent excessif si la section critique ne constitue qu'une petite partie de la méthode et que le reste du code ne nécessite pas de synchronisation.
Un bloc synchronized offre un contrôle précis : vous spécifiez l'objet moniteur et synchronisez uniquement la section de code nécessaire, laissant le reste de la méthode en dehors du verrouillage. Cela minimise le temps de détention du moniteur et améliore les performances globales de l'application dans un environnement multithread, car d'autres threads peuvent exécuter du code non lié en parallèle sans attendre la libération du moniteur.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// code hors de la section critique - sans synchronisation
prepareData()
synchronized (lock) {
// seul ce bloc est protégé
updateSharedState()
}
// suite sans verrouillage
cleanup()
}
}
| Critère | Méthode synchronized | Bloc synchronized |
|---|---|---|
| Moniteur | this (instance) ou Class | n'importe quel objet |
| Granularité | méthode entière | seulement le code nécessaire |
| Lisibilité | élevée | moyenne |
| Performance | inférieure pour les grandes méthodes | supérieure pour les petites sections critiques |
Dans le développement Android, synchronized est largement utilisé pour protéger SharedPreferences, l'accès aux bases de données et les composants d'interface utilisateur. Cependant, son utilisation sur le thread principal est fortement déconseillée en raison du risque de blocage de l'interface.
SharedPreferences dans Android offre une sécurité de base entre les threads, mais lors de l'édition depuis plusieurs threads via Editor, une synchronisation externe peut être nécessaire. Un bloc synchronized avec un objet de verrouillage séparé garantit la cohérence des modifications.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
La principale limitation de synchronized sur Android est le blocage de thread. Contrairement aux coroutines avec Mutex, synchronized bloque complètement le thread système. Sur le thread principal, cela provoque une ANR. Dans le développement Android moderne, il est recommandé de remplacer synchronized par des coroutines (suspend Mutex) ou des types atomiques (AtomicInteger).
Java et Kotlin modernes offrent plusieurs alternatives à synchronized, chacune résolvant les mêmes problèmes avec moins de limitations ou de meilleures performances.
L'interface Lock avec les implémentations ReentrantLock et ReadWriteLock fournit des timeouts, une attente interruptible et plusieurs files Condition. Elle est plus flexible que synchronized mais nécessite une libération explicite dans finally, ce qui augmente le risque d'erreur en cas d'oubli du unlock.
AtomicInteger, AtomicLong, AtomicReference et d'autres classes utilisent des algorithmes Lock-Free basés sur CAS (Compare-And-Swap). Elles sont significativement plus rapides que synchronized dans les scénarios de contention modérée car elles ne bloquent pas les threads mais effectuent des tentatives optimistes sans nécessiter de changement de contexte du noyau du système d'exploitation.
ThreadLocal offre une approche alternative : chaque variable ThreadLocal est isolée au sein d'un seul thread et ne nécessite pas de synchronisation pour la lecture et l'écriture. Cela élimine complètement le besoin de synchronized pour les données qui ne doivent pas être partagées entre les threads. ThreadLocal est activement utilisé dans les frameworks (Spring, Hibernate) pour stocker le contexte des transactions et des sessions.
Dans les projets Kotlin pour Android, une alternative à synchronized est Mutex de kotlinx.coroutines. Il ne bloque pas le thread du système d'exploitation mais suspend la coroutine jusqu'à la libération du verrou, ce qui permet une utilisation efficace des threads du pool et évite les ANR lors de longues attentes de libération de ressource.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
Les performances de synchronized ont considérablement changé dans les versions récentes de Java. Avant, il était considéré comme un mécanisme « lourd », mais les JVM modernes ont éliminé la plupart des surcharges grâce aux optimisations avancées du compilateur JIT. Voyons en détail comment la machine virtuelle accélère le code synchronisé à l'exécution.
Le compilateur JIT de la JVM applique plusieurs optimisations : biased locking élimine la synchronisation si le verrou est toujours acquis par le même thread ; lock coarsening fusionne les blocs synchronized adjacents en un seul ; lock elimination supprime la synchronisation si l'objet n'est accessible que par un seul thread. Ces optimisations rendent synchronized pratiquement gratuit en cas de faible contention.
La JVM détermine le niveau de contention pour chaque objet : en l'absence de contention, le biased locking est activé ; lorsqu'un second thread apparaît, le verrou passe en mode léger avec attente active (spin) ; et ce n'est qu'en cas d'attente prolongée qu'il escalade vers le mode lourd avec un mutex système. Cette escalade se produit automatiquement, et le développeur n'a pas besoin de choisir manuellement une stratégie.
Dans les benchmarks modernes (Java 17+), synchronized montre des performances comparables à ReentrantLock avec une contention faible et modérée. En cas de forte contention, Lock peut avoir l'avantage grâce à une file d'attente plus efficace avec prise en charge des timeouts et des interruptions. Pour les systèmes à charge élevée où la contention est constante, ReentrantLock en mode fair offre un comportement plus prévisible.
Les classes atomiques (AtomicInteger, AtomicReference) restent les plus rapides pour les compteurs et indicateurs simples grâce à l'implémentation Lock-Free basée sur CAS. Elles ne bloquent pas du tout les threads : en cas de conflit, l'opération se répète simplement dans une boucle. Cela donne un gain de performance de 3 à 5 fois par rapport à synchronized sur les opérations d'incrémentation de compteur avec 4 à 8 threads.
Questions fréquentes
Synchronized fournit à la fois l'exclusion mutuelle et la visibilité. Volatile ne garantit que la visibilité des modifications : l'écriture dans une variable volatile est visible par tous les threads mais n'empêche pas la modification simultanée, c'est-à-dire qu'elle ne protège pas contre les conditions de course.
Oui, un deadlock est possible avec une synchronisation imbriquée utilisant un ordre de moniteurs différent. Par exemple, un thread appelle synchronized(a) { synchronized(b) }, tandis qu'un autre appelle synchronized(b) { synchronized(a) }. Évitez les blocs synchronized imbriqués ou fixez un ordre de moniteurs cohérent.
Un moniteur est un mécanisme de synchronisation associé à chaque objet Java. Il garantit qu'un seul thread exécute du code synchronized sur cet objet. Le moniteur comprend un verrou, une file d'attente et un ensemble de threads attendant une notification via wait/notify.
Dans les versions modernes de Java (17+), synchronized n'est pas en reste face à Lock en termes de performances grâce aux optimisations JIT (biased locking, lock coarsening). Lock est préféré non pas pour la vitesse mais pour les fonctionnalités supplémentaires : timeouts, attente interruptible et plusieurs files Condition.
Une méthode statique synchronized utilise le moniteur de l'objet Class de la classe donnée, et non de l'instance. Cela signifie que la synchronisation s'applique à toutes les instances de la classe. Les méthodes synchronized non statiques et statiques utilisent des moniteurs différents et ne se bloquent pas mutuellement.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi