Le langage Java fournit un mécanisme de synchronisation permettant de partager des ressources entre plusieurs threads. Cela a pour effet d'éviter des incohérences dans le fonctionnement d'un système multithreads, telles que la corruption des données partagées.
Les méthodes contenant du code critique, c'est à dire un ensemble d'opérations atomiques agissant sur des données sensibles, doivent être synchronisées.
Les opérations atomiques constituent des tâches qui ne fournissent pas de résultats intermédiaires. Par exemple, l'ajout d'un élément au sein d'une liste contenant des objets d'un type spécifique et dont la taille est limitée, demanderait :
public static final int LIMITE = 10; public boolean verifierType(Object element){ return element instanceof Element; } public boolean verifierPlace(){ return this.size() < LIMITE; } public boolean ajouter(Object element) throws CollectionCompleteException { if(verifierPlace()) throw new CollectionCompleteException(); return this.add(element); } public void ajouter(Object element){ if(verifierType(element)){ if(verifierPlace()){ this.ajouter(element); } } }
Cet exemple montre trois opérations qui fournissent trois résultats distincts, mais dont les opérations suivantes dépendent.
Dans une application multithreads :
L'état d'un objet partagé est donc susceptible de changer en cours d'exécution et d'une façon inattendue. Cela pourrait provoquer de graves dysfonctionnements dans un programme. Dans ce cas, la méthode ajouter() doit devenir atomique, afin que le résultat intermédiaire produit par la méthode verifierPlace() soit identique pour le reste des instructions. De cette manière, un seul thread serait en mesure d'accéder à cette méthode à un moment donné.
Dans le cas de données partagées, ce genre de décomposition du code doit être impérativement prise en compte en vue de déterminer à quel moment des variables sensibles riquent d'être modifiées ou accèdées par des threads concurrents.
La solution pour protéger ces variables réside dans la synchronisation des méthodes ou des blocs de code agissant directement sur elles. La synchronisation consiste à simuler l'atomicité sur des sections critiques, et par voie de conséquence, à distribuer un verrou aux threads pour accèder à des zones synchronisées. De cette manière, un seul thread possède l'accès exclusif à une section critique. Les autres threads sont obligés de patienter jusqu'à ce qu'ils obtiennent à leur tour le verrou libéré suite à la fin de l'exécution de la zone synchronisée. De plus, toutes les sections critiques d'un objet, sont bloqués tant que le thread courant ne restitue pas le verou.
public synchronized uneMethode(){ //Traitement de données sensibles... } public void uneAutreMethode(){ synchronized(objet){ //Traitement de données sensibles... } } public synchronized void ajouter(Object element){ if(verifierType(element)){ if(verifierPlace()){ this.ajouter(element); } } } //ou public void ajouter(Object element){ if(verifierType(element)){ synchronized(this){ if(verifierPlace()){ this.ajouter(element); } } } }