Salut,
Pour moi il n'y a pas de problème. : le mot-clef synchronized n'est pas le seul à permettre un code thread-safe
Je m'explique : on est d'accord sur le fait que les méthodes add() et remove() doivent être synchronisé pour éviter des "pertes" de données...
Toutefois, si tu regardes bien le code de ces deux méthodes, tu t'aperçois qu'elles travailles sur un tableau d'objets temporaire, et que l'attribut listenerList n'est modifié qu'à la fin du traitement. Par exemple dans la méthode add() on peut trouver ceci (j'ai enlevé les vérifications et cas particuliers) :
1 2 3 4 5 6 7 8 9
| // Otherwise copy the array and add the new listener
int i = listenerList.length;
Object[] tmp = new Object[i+2];
System.arraycopy(listenerList, 0, tmp, 0, i);
tmp[i] = t;
tmp[i+1] = l;
listenerList = tmp; |
Ainsi à chaque opération add() ou remove(), un nouveau tableau est créé, et ensuite sa référence est assigné dans l'attribut listenerList. Or l'assignement est forcément une opération atomique (sauf pour les types long et double où cela peut dépendre de l'implémentation car ils sont codés sur 64bits).
Et si tu regardes bien la méthodes getListeners(), elle copie la référence de listenerList dans une variable locale :
Object[] lList = listenerList;
Cette opératio n'a pas besoin d'être synchronisé puisque l'accès à listenerList est une opération atomique.
Et comme le reste de la méthode travaille sur la variable locale lList. De ce fait si un add() ou remove() est appelé dans un autre thread, cela modifiera l'attribut listenerList mais n'aura aucun impact sur notre méthode getListeners() car elles travaille sur une copie :
[list][*]Dans le meilleur des cas le add()/remove() modifie la référence de listenerList juste avant l'affection dans lList, et dans ce cas la méthode getListeners() travaillera avec les toutes dernières données.[*]Dans le pire des cas, listenerList sera modifié entre l'affection de lList et la boucle de traitement, et dans ce cas lList correspond à une version "obsolète" de listenerList, et possèdera alors un listener en plus ou en moins... mais cela ne pose pas de problème d'exécution et est acceptable (grosso modo cela reviendrait à ce que la méthode add()/remove() soit exécuté après le getListeners()).
Le fait de ne modifier la référence de l'attribut qu'à la fin des méthodes add()/remove(), et d'utiliser une copie de la référence dans les autres méthodes est une optimisation permettant d'éviter de tout synchroniser.
Un exemple un peu plus explicite :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class MaClasse {
String value;
public synchronized void setValue(String value) {
this.value = value.trim();
// On remplace les caractères spéciaux HTML :
this.value = this.value.replaceAll("&", "&");
this.value = this.value.replaceAll("<", "<");
this.value = this.value.replaceAll(">", ">");
// On remplace les fin de ligne par des <BR/>
this.value = this.value.replaceAll("\n", "<BR/>");
// On supprime les espaces multiples :
this.value = this.value.replaceAll("\\s+", " ");
}
public synchronized String getValue() {
return this.value;
}
public synchronized void method() {
// Plusieurs traitements sur this.value
}
} |
Dans ce code, on est obligé de synchroniser les trois méthodes, sinon on pourrait avoir une valeur de this.value lorsqu'on y accède entre deux replaceAll()...
Le problème c'est que toutes les méthodes de la classe qui utiliseront l'attribut this.value devront être synchronisé... ce qui rend le tout plus complexe et peut multiplier les 'conflits' sur les verrous...
Or, dans ce cas, il est possible de faire un code thread-safe sans utiliser le mot-clef synchronized.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class MaClasse {
String value;
public void setValue(String value) {
String lValue = value.trim();
// On remplace les caractères spéciaux HTML :
lValue = lValue.replaceAll("&", "&");
lValue = lValue.replaceAll("<", "<");
lValue = lValue.replaceAll(">", ">");
// On remplace les fin de ligne par des <BR/>
lValue = lValue.replaceAll("\n", "<BR/>");
// On supprime les espaces multiples :
lValue = lValue.replaceAll("\\s+", " ");
// On ne modifie l'attribut qu'à la fin du traitement :
this.value = lValue;
}
public String getValue() {
return this.value;
}
public void method() {
String lValue = this.value;
// Plusieurs traitement sur lValue
}
} |
On respecte seulement deux règles simples :
- Chaque méthode qui modifie l'attribut travaille sur une variable locale, et n'affecte la référence de l'attribut qu'à la fin de son traitement (une fois que la valeur est correcte).
- Chaque méthode qui utilise l'attribut n'effecte qu'un accès à sa référence, en effectuant une copie dans une variable locale s'il a besoin d'effectuer plusieurs traitements.
Et dans le pire des cas on travaille sur une copie 'obsolète' de l'attribut, mais cela peut être acceptable dans bien des cas (et permet d'éviter de tout synchronisé).
a++
Partager