La concurrence en pratique – I
Dans mes résolutions post-Devoxx, j’ai prévu d’écrire une suite d’article sur la concurrence, destiné à tous ceux qui ne sont pas très à l’aise avec les races conditions et autres sympathies. Comme c’est le week-end de Pâques, ce premier article sera assez simple.
Si on considère la classe Color qui comporte 2 méthode : une qui change les 2 champs preferedColor et colorOfMyDress avec la même couleur donnée en paramètre et une qui affiche les 2 champs.
public class Color { public String preferedColor; public String colorOfMyDress; public void changeColor(String color) { preferedColor = color; colorOfMyDress = color; } public void printColor() { System.out.println("Ma couleur préferée est le " + preferedColor + " et la couleur de ma robe est " + colorOfMyDress); } }
Imaginons une classe AuroreColor qui ressemble à celle la et que cette classe soit utilisée dans une appli web, et l’on souhaite que tous les utilisateurs puissent modifier la couleur qui est partagée entre tous :
public class AuroreColor { public Color color; ... }
Chaque utilisateur peut changer la couleur via une interface et en réponse, s’affiche le texte affiché par la méthode printColor(). Si Paul choisit et valide la couleur rouge, alors il voit à l’écran : « Ma couleur préferée est le rouge et la couleur de ma robe est rouge. »
Mais si 2 utilisateurs utilisent en même temps cette fonctionnalité, on peut obtenir des résultats « non attendus » comme « Ma couleur préferée est le vert et la couleur de ma robe est jaune », alors qu’on s’attends à ce que les 2 champs soient identiques.
En effet, il est (entre autre) possible que au même moment, les threads associés à chacun des utilisateurs soient au même moment au sein de la méthode changeColor, impliquant que pour un laps de temps très court, les 2 paramètres sont modifiés et ont des valeurs différentes. C’est particulièrement visualisable si on reprends l’exemple d’un counter :
public class Counter {
double counter; public void increment() { counter++; } public double getCounter() { return counter; } }
On essaie de compter le nombre de mots d’un livre mais on se rends compte que le résultat n’est pas toujours correct. La cause est assez simple : counter++ marche de la façon suivante :
– Je récupère la valeur de counter
– J’ajoute 1
– J’affecte le résultat à counter
Si 2 threads accèdent l’un après l’autre à ce code, pas de problème. Par contre, si 2 threads appelent la méthode incrémente dans un laps de temps très court, alors il est possible qu’ils utilisent la même valeur de counter lors de l’incrémentation, alors, chacun va faire un +1, sur la même valeur et réassignée cette valeur à counter. Ainsi, on aura au final, counter = 1 alors que l’un après l’autre, on voit bien counter = 2.
En séquence on aura :
Thread 1 :
Valeur de counter initial : 0
Valeur après incrémentation : 1
Thread 2 :
Valeur de counter initial : 1
Valeur après incrémentation : 2
Soit un résultat final de 2
En parrallèle, on peut avoir :
Thread 1 :
Valeur de counter initial : 0
Valeur après incrémentation : 1
Thread 2 :
Valeur de counter initial : 0
Valeur après incrémentation : 1
Soit un résultat final de 1
Il y a plusieurs manières de résoudre ce problème. L’un d’elle serait de garantir que 2 threads ne puissent pas accéder en même temps à la méthode changeColor (ou increment) en utilisant le mot clé synchronized sur cette méthode . Si on reprends les explications pour une méthode synchronized :
- First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.
- Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.
Pour faire plus simple, le mot clé synchronized sur une méthode garantit que cette dernière ne sera exécutée qu’un par au plus un thread, les autres attendant que le premier ait finit.
public class Color { public String preferedColor; public String colorOfMyDress; public synchronized void changeColor(String color) { preferedColor = color; colorOfMyDress = color; } public void printColor() { System.out.println("Ma couleur préferée est le " + preferedColor + " et la couleur de ma robe est " + colorOfMyDress); } }
En modifiant de la sorte, 2 threads ne pourront jamais modifier en même temps les couleurs, mais seulement l’un après l’autre. Mais il y a un mais, car cela ne marche pas et cela fait partie des erreurs que l’on retrouve souvent
Je lis avec attention ce billet car j’utilise précisement cette methode. J’attends donc de savoir le fameux mais…
Bonjour Mathilde,
Pourquoi est-ce que tu dis que ça ne marche pas ? Est-ce que tu penses au cas de serveurs en cluster ou même pour un serveur tout seul ça ne fonctionne pas ?
Je passais par là, au hasard de google, et je me permet de répondre, car ce petit bout de code… c’est bien essayé, mais ce n’est pas thread-safe
En effet, la pose d’un synchronized sur le setter permet de s’assurer que les appels à cette méthodes seront serialisés (comprendre: exécuté en série, les uns à la suite des autres), mais aussi que le contenus des variables seront bien « publiée » entre les threads qui vont accéder à cette méthode.
Or, ce n’est pas le cas de « printColor ».
Imaginons un Thread 3, qui ne met jamais à jour la couleur, mais qui la lis à interval régulier.
Et bien il va avoir des mises à jour des valeurs aléatoires (en théorie, il peut même ne jamais voir les modifications).
Un thread qui ferait (comme présenté dans l’article) de la mise à jour avec l’appel à changeColor, puis une lecture avec printColor n’aurra certainement aucun problème.
Ce ne sera pas le cas d’un thread qui ne ferait que de la lecture, ou qui ferait une lecture AVANT l’écriture.
Bref, il faut également poser un « synchronized » sur la méthode printColor.
Pour résumer : Protéger son code des accès concurrents nécessite à la fois de protéger ses méthodes en écriture, mais également en lecture !
Les variables preferedColor et colorOfMyDress étant publiques, n’importe qui peut les modifier sans passer par la méthode changeColor.
Il faudrait par exemple les mettre privées.