Vous demandez spécifiquement comment ils fonctionnent en interne , alors vous êtes ici:
Pas de synchronisation
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
Il lit essentiellement la valeur de la mémoire, l'incrémente et la remet en mémoire. Cela fonctionne sur un seul thread, mais de nos jours, à l'ère des caches multicœurs, multi-CPU et multi-niveaux, cela ne fonctionnera pas correctement. Tout d'abord, il introduit la condition de concurrence (plusieurs threads peuvent lire la valeur en même temps), mais aussi des problèmes de visibilité. La valeur peut uniquement être stockée dans la mémoire CPU " locale " (un cache) et ne pas être visible pour les autres CPU / cœurs (et donc - les threads). C'est pourquoi beaucoup font référence à une copie locale d'une variable dans un thread. C'est très dangereux. Considérez ce code d'arrêt de fil populaire mais cassé:
private boolean stopped;
public void run() {
while(!stopped) {
//do some work
}
}
public void pleaseStop() {
stopped = true;
}
Ajoutez volatile
à la stopped
variable et cela fonctionne très bien - si tout autre thread modifie la stopped
variable via la pleaseStop()
méthode, vous êtes assuré de voir ce changement immédiatement dans la while(!stopped)
boucle du thread de travail . BTW ce n'est pas non plus un bon moyen d'interrompre un thread, voir: Comment arrêter un thread qui s'exécute pour toujours sans aucune utilisation et Arrêter un thread java spécifique .
AtomicInteger
private AtomicInteger counter = new AtomicInteger();
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
La AtomicInteger
classe utilise des opérations CPU de bas niveau CAS ( comparer et échanger ) (aucune synchronisation nécessaire!). Elles vous permettent de modifier une variable particulière uniquement si la valeur actuelle est égale à autre chose (et est retournée avec succès). Ainsi, lorsque vous l'exécutez, getAndIncrement()
il s'exécute en fait en boucle (implémentation réelle simplifiée):
int current;
do {
current = get();
} while(!compareAndSet(current, current + 1));
Donc, fondamentalement: lire; essayez de stocker la valeur incrémentée; en cas d'échec (la valeur n'est plus égale à current
), lisez et réessayez. Le compareAndSet()
est implémenté en code natif (assembly).
volatile
sans synchronisation
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
Ce code n'est pas correct. Il corrige le problème de visibilité ( volatile
garantit que les autres threads peuvent voir les modifications apportées counter
), mais a toujours une condition de concurrence. Cela a été expliqué plusieurs fois: la pré / post-incrémentation n'est pas atomique.
Le seul effet secondaire de volatile
" vidage " des caches pour que toutes les autres parties voient la version la plus récente des données. C'est trop strict dans la plupart des situations; c'est pourquoi volatile
n'est pas par défaut.
volatile
sans synchronisation (2)
volatile int i = 0;
void incIBy5() {
i += 5;
}
Le même problème que ci-dessus, mais encore pire car ce i
n'est pas le cas private
. La condition de course est toujours présente. Pourquoi est-ce un problème? Si, par exemple, deux threads exécutent ce code simultanément, la sortie peut être + 5
ou + 10
. Cependant, vous êtes assuré de voir le changement.
Multiple indépendant synchronized
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Surprise, ce code est également incorrect. En fait, c'est complètement faux. Tout d'abord, vous synchronisez i
, ce qui est sur le point d'être modifié (en plus, i
c'est une primitive, donc je suppose que vous synchronisez sur un temporaire Integer
créé via l'autoboxing ...) Complètement défectueux. Vous pouvez également écrire:
synchronized(new Object()) {
//thread-safe, SRSLy?
}
Deux threads ne peuvent pas entrer dans le même synchronized
bloc avec le même verrou . Dans ce cas (et de la même manière dans votre code), l'objet verrou change à chaque exécution, donc synchronized
n'a effectivement aucun effet.
Même si vous avez utilisé une variable finale (ou this
) pour la synchronisation, le code est toujours incorrect. Deux threads peuvent d'abord lire i
de façon temp
synchrone (ayant la même valeur localement temp
), puis le premier attribue une nouvelle valeur à i
(par exemple, de 1 à 6) et l'autre fait la même chose (de 1 à 6).
La synchronisation doit s'étendre de la lecture à l'attribution d'une valeur. Votre première synchronisation n'a aucun effet (la lecture d'un int
est atomique) et la seconde également. À mon avis, ce sont les formes correctes:
void synchronized incIBy5() {
i += 5
}
void incIBy5() {
synchronized(this) {
i += 5
}
}
void incIBy5() {
synchronized(this) {
int temp = i;
i = temp + 5;
}
}