Aujourd'hui, une opération délicate dans mon laboratoire s'est complètement mal passée. Un actionneur sur un microscope électronique a dépassé ses limites et, après une chaîne d'événements, j'ai perdu 12 millions de dollars d'équipement. J'ai réduit plus de 40 000 lignes dans le module défectueux à ceci:
import java.util.*;
class A {
static Point currentPos = new Point(1,2);
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
new Thread() {
void f(Point p) {
synchronized(this) {}
if (p.x+1 != p.y) {
System.out.println(p.x+" "+p.y);
System.exit(1);
}
}
@Override
public void run() {
while (currentPos == null);
while (true)
f(currentPos);
}
}.start();
while (true)
currentPos = new Point(currentPos.x+1, currentPos.y+1);
}
}
Quelques échantillons de la sortie que je reçois:
$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651
Puisqu'il n'y a pas d'arithmétique à virgule flottante ici, et nous savons tous que les entiers signés se comportent bien en cas de débordement en Java, je pense qu'il n'y a rien de mal à ce code. Cependant, malgré la sortie indiquant que le programme n'a pas atteint la condition de sortie, il a atteint la condition de sortie (il était à la fois atteint et non atteint?). Pourquoi?
J'ai remarqué que cela ne se produit pas dans certains environnements. Je suis sur OpenJDK 6 sur Linux 64 bits.
final
qualificatif (qui n'a aucun effet sur le bytecode produit) dans les champsx
ety
"résout" le bug. Bien que cela n'affecte pas le bytecode, les champs sont marqués avec lui, ce qui m'amène à penser que c'est un effet secondaire d'une optimisation JVM.Point
p
est construit qui satisfaitp.x+1 == p.y
, puis une référence est transmise au thread d'interrogation. Finalement, le thread d'interrogation décide de quitter, car il pense que la condition n'est pas satisfaite pour l'un desPoint
s qu'il reçoit, mais la sortie de la console montre alors qu'il aurait dû être satisfait. L'absence d'volatile
ici signifie simplement que le fil d'interrogation peut rester bloqué, mais ce n'est clairement pas le problème ici.synchronized
empêche le bug de se produire? C'est parce que j'ai dû écrire au hasard du code jusqu'à ce que j'en trouve un qui reproduise ce comportement de manière déterministe.Réponses:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
fait quelques choses, y compris l'écriture de valeurs par défaut dansx
ety
(0), puis l'écriture de leurs valeurs initiales dans le constructeur. Étant donné que votre objet n'est pas publié en toute sécurité, ces 4 opérations d'écriture peuvent être librement réorganisées par le compilateur / JVM.Du point de vue du thread de lecture, c'est donc une exécution légale à lire
x
avec sa nouvelle valeur maisy
avec sa valeur par défaut de 0 par exemple. Au moment où vous atteignez l'println
instruction (qui est d'ailleurs synchronisée et influence donc les opérations de lecture), les variables ont leurs valeurs initiales et le programme imprime les valeurs attendues.Marquer
currentPos
commevolatile
garantira une publication sûre puisque votre objet est effectivement immuable - si dans votre cas d'utilisation réel l'objet est muté après la construction, lesvolatile
garanties ne seront pas suffisantes et vous pourriez voir à nouveau un objet incohérent.Alternativement, vous pouvez rendre l'
Point
immuable qui assurera également une publication sûre, même sans utilisationvolatile
. Pour atteindre l'immuabilité, il vous suffit de marquerx
et dey
terminer.En remarque et comme déjà mentionné,
synchronized(this) {}
peut être traité comme un no-op par la JVM (je comprends que vous l'avez inclus pour reproduire le comportement).la source
Comme il
currentPos
est modifié en dehors du fil, il doit être marqué commevolatile
:Sans volatilité, le thread n'est pas garanti de lire les mises à jour de currentPos qui sont effectuées dans le thread principal. Par conséquent, de nouvelles valeurs continuent d'être écrites pour currentPos, mais le thread continue d'utiliser les versions mises en cache précédentes pour des raisons de performances. Étant donné qu'un seul thread modifie currentPos, vous pouvez vous en tirer sans verrous, ce qui améliorera les performances.
Les résultats sont très différents si vous ne lisez les valeurs qu'une seule fois dans le thread pour les utiliser dans la comparaison et leur affichage ultérieur. Lorsque je fais ce qui suit
x
s'affiche toujours en tant que1
ety
varie entre0
et un grand entier. Je pense que le comportement de celui-ci à ce stade est quelque peu indéfini sans levolatile
mot - clé et il est possible que la compilation JIT du code y contribue. De plus, si je commente lesynchronized(this) {}
bloc vide , le code fonctionne également et je soupçonne que c'est parce que le verrouillage provoque un retard suffisantcurrentPos
et que ses champs sont relus plutôt qu'utilisés à partir du cache.la source
volatile
.Vous avez une mémoire ordinaire, la référence 'currentpos' et l'objet Point et ses champs derrière elle, partagés entre 2 threads, sans synchronisation. Ainsi, il n'y a pas d'ordre défini entre les écritures qui arrivent à cette mémoire dans le thread principal et les lectures dans le thread créé (appelez-le T).
Le thread principal effectue les écritures suivantes (en ignorant la configuration initiale de point, px et py auront des valeurs par défaut):
Parce qu'il n'y a rien de spécial à propos de ces écritures en termes de synchronisation / barrières, le runtime est libre pour permettre au thread T de les voir se produire dans n'importe quel ordre (le thread principal voit bien sûr toujours les écritures et les lectures ordonnées selon l'ordre du programme), et se produit à tout moment entre les lectures de T.
Alors T fait:
Étant donné qu'il n'y a pas de relations de classement entre les écritures dans main et les lectures dans T, il existe clairement plusieurs façons de produire votre résultat, car T peut voir l'écriture de main dans currentpos avant les écritures dans currentpos.y ou currentpos.x:
et ainsi de suite ... Il y a un certain nombre de courses de données ici.
Je soupçonne l'hypothèse erronée ici de penser que les écritures qui résultent de cette ligne sont rendues visibles sur tous les threads dans l'ordre du programme du thread qui l'exécute:
Java n'offre aucune garantie (ce serait terrible pour les performances). Quelque chose de plus doit être ajouté si votre programme a besoin d'un ordre garanti des écritures par rapport aux lectures dans d'autres threads. D'autres ont suggéré de rendre les champs x, y définitifs ou de rendre volatils les courants.
L'utilisation de final présente l'avantage de rendre les champs immuables et permet ainsi la mise en cache des valeurs. L'utilisation de données volatiles entraîne une synchronisation à chaque écriture et lecture des positions actuelles, ce qui peut nuire aux performances.
Voir le chapitre 17 de la spécification du langage Java pour les détails sanglants: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
(La réponse initiale supposait un modèle de mémoire plus faible, car je n'étais pas sûr que la volatilité garantie par JLS était suffisante. ).
la source
currentPos
est rendu volatil, l'affectation garantit la publication sûre de l'currentPos
objet ainsi que de ses membres, même s'ils ne sont pas volatils eux-mêmes.Point currentPos = new Point(x, y)
, vous avez 3 écritures: (w1)this.x = x
, (w2)this.y = y
et (w3)currentPos = the new point
. L'ordre du programme garantit que hb (w1, w3) et hb (w2, w3). Plus tard dans le programme, vous lisez (r1)currentPos
. S'ilcurrentPos
n'est pas volatil, il n'y a pas de hb entre r1 et w1, w2, w3, donc r1 pourrait en observer une (ou aucune). Avec volatile, vous introduisez hb (w3, r1). Et la relation hb étant transitive, vous introduisez également hb (w1, r1) et hb (w2, r1). Ceci est résumé dans Java Concurrency in Practice (3.5.3. Safe Publication Idioms).Vous pouvez utiliser un objet pour synchroniser les écritures et les lectures. Sinon, comme d'autres l'ont déjà dit, une écriture dans currentPos se produira au milieu des deux lectures p.x + 1 et py
la source
sem
n'est pas partagée et traiter l'instruction synchronisée comme un no-op ... Le fait qu'elle résout le problème est une pure chance.Vous accédez à currentPos deux fois et ne garantissez pas qu'il n'est pas mis à jour entre ces deux accès.
Par exemple:
Vous comparez essentiellement deux différents points .
Notez que même rendre currentPos volatile ne vous protégera pas de cela, car il s'agit de deux lectures distinctes par le thread de travail.
Ajouter un
à votre classe de points. Cela garantira qu'une seule valeur de currentPos est utilisée lors de la vérification de x + 1 == y.
la source