« Que signifie synchronized ? » — la question qui hante un junior sur deux
\n\nSi vous avez déjà écrit une application multithread en Java, vous avez sûrement été confronté à cette situation : deux threads modifient simultanément une même variable, et le résultat — des bugs qui ne se reproduisent qu'une fois sur cent exécutions. Cela vous dit quelque chose ? C'est exactement pour cela que le mot-clé synchronized existe. Mais que fait-il réellement ? Comment fonctionne-t-il « sous le capot » ? Et surtout — quand l'utiliser, et quand vaut-il mieux s'en passer ?
Dans cet article, nous ne nous contenterons pas de répéter la documentation. Nous analyserons une question populaire de Stack Overflow (celle qui a recueilli des milliers de vues), nous l'adapterons à un format éducatif et montrerons une comparaison des approches : synchronisation au niveau de la méthode, au niveau du bloc, et les alternatives de java.util.concurrent. C'est parti !
Le problème : la course aux threads (Race Condition)
\n\nImaginez : vous avez un compteur que deux threads incrémentent. Le code est simple, mais le résultat est inattendu.
\n\npublic class Counter {\n private int count = 0;\n\n public void increment() {\n count++;\n }\n\n public int getCount() {\n return count;\n }\n}\n\nLançons deux threads, chacun effectue 1000 incréments. On s'attendrait à 2000. Mais en réalité — 1987, 1993, ou même 1765. Pourquoi ? Parce que l'opération count++ n'est pas atomique. Elle se compose de trois étapes : lecture → incrémentation → écriture. Les threads « se chevauchent », et une modification est perdue.
C'est ce qu'on appelle une race condition — une condition de course. Et synchronized est l'un des principaux outils pour la combattre.
Que fait synchronized ? Regardons sous le capot
\n\nLe mot-clé synchronized en Java garantit l'exclusion mutuelle (mutex) et la visibilité des modifications entre les threads. Lorsqu'un thread entre dans un bloc synchronized, il verrouille le moniteur de l'objet (ou de la classe). Les autres threads qui tentent d'entrer dans un bloc synchronized sur le même moniteur sont bloqués jusqu'à sa libération.
Important : synchronized ne fonctionne qu'avec un seul moniteur. Si vous avez deux objets différents — la synchronisation ne fonctionnera pas.
Deux niveaux de synchronisation
\n- \n
- Méthode synchronisée — verrouille toute la méthode, utilise le moniteur de l'objet (
this). \n - Bloc synchronisé — verrouille seulement une partie du code, vous pouvez spécifier n'importe quel objet comme moniteur. \n
Comparons-les en pratique.
\n\nComparaison des approches : méthode vs bloc
\n\n1. Méthode synchronisée
\n\nLa manière la plus simple — ajouter synchronized à la signature de la méthode.
public class SafeCounter {\n private int count = 0;\n\n public synchronized void increment() {\n count++;\n }\n\n public synchronized int getCount() {\n return count;\n }\n}\n\nAvantages :
\n- \n
- Code minimal, facile à lire. \n
- Garantit que toute la méthode est une opération atomique. \n
Inconvénients :
\n- \n
- Verrouillage « lourd » — même si la méthode ne contient qu'une seule ligne, le thread est bloqué pendant toute la durée d'exécution. \n
- Réduit les performances si la méthode est longue ou appelée fréquemment. \n
- Pas flexible — impossible de synchroniser seulement une partie de la logique. \n
2. Bloc synchronisé
\n\nUn contrôle plus fin. Vous décidez vous-même quel objet utiliser comme moniteur.
\n\npublic class SafeCounterWithBlock {\n private int count = 0;\n private final Object lock = new Object();\n\n public void increment() {\n synchronized (lock) {\n count++;\n }\n }\n\n public int getCount() {\n synchronized (lock) {\n return count;\n }\n }\n}