Vous avez une mémoire ordinaire, la référence 'currentpos' et l'objet Point et ses champs derrière elle, partagée entre 2 threads, sans synchronisation. Ainsi, il n'y a pas d'ordre défini entre les écritures qui se produisent dans cette mémoire dans le thread principal et les lectures dans le thread créé (appelé T).
Le fil principal effectue les écritures suivantes (en ignorant la configuration initiale du point, il en résulte que p.x et p.y ont des valeurs par défaut) :
- à p.x
- à p.y
- à la position actuelle
Comme ces écritures n'ont rien de spécial en termes de synchronisation/barrières, le runtime est libre de 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 de les faire se produire à n'importe quel moment entre les lectures dans T.
Donc T fait :
- lit la position actuelle dans p
- lire p.x et p.y (dans un ordre quelconque)
- comparer, et prendre la branche
- lire p.x et p.y (dans n'importe quel ordre) et appeler System.out.println
Etant donné qu'il n'y a pas de relations d'ordre entre les écritures dans main, et les lectures dans T, il y a clairement plusieurs façons de produire votre résultat, car T peut voir l'écriture de main à currentpos avant l'écriture à currentpos.y ou currentpos.x :
- Il lit d'abord currentpos.x, avant que l'écriture x ne se produise - obtient 0, puis lit currentpos.y avant que l'écriture y ne se produise - obtient 0. Comparez les résultats à true. Les écritures deviennent visibles pour T. System.out.println est appelé.
- Il lit d'abord currentpos.x, après que l'écriture x se soit produite, puis lit currentpos.y avant que l'écriture y se soit produite - il obtient 0. La comparaison est vraie. Les écritures deviennent visibles pour T... etc.
- Il lit d'abord currentpos.y, avant que l'écriture y ne se produise (0), puis lit currentpos.x après l'écriture x, évalue à true. etc.
et ainsi de suite... Il y a un certain nombre de courses aux données ici.
Je pense que l'hypothèse erronée ici est de penser que les écritures qui résultent de cette ligne sont rendues visibles à travers tous les threads dans l'ordre du programme du thread qui l'exécute :
currentPos = new Point(currentPos.x+1, currentPos.y+1);
Java n'offre pas une telle 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 finaux, ou alternativement de rendre currentpos volatile.
- Si vous rendez les champs x,y finaux, Java garantit que l'écriture de leurs valeurs sera vue comme ayant lieu avant le retour du constructeur, dans tous les threads. Ainsi, comme l'affectation à currentpos se fait après le constructeur, le thread T est assuré de voir les écritures dans le bon ordre.
- Si vous rendez currentpos volatile, Java garantit que c'est un point de synchronisation qui sera totalement ordonné par rapport aux autres points de synchronisation. Comme dans main, les écritures sur x et y doivent se produire avant l'écriture sur currentpos, alors toute lecture de currentpos dans un autre thread doit également voir les écritures de x, y qui se sont produites avant.
L'utilisation de final a l'avantage de rendre les champs immuables, et donc de permettre la mise en cache des valeurs. L'utilisation de volatile entraîne une synchronisation à chaque écriture et lecture de currentpos, 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 le volatile garanti par JLS était suffisant. Réponse modifiée pour refléter le commentaire d'assylias, indiquant que le modèle Java est plus fort - happens-before est transitif - et donc volatile sur currentpos suffit aussi).
41 votes
12 millions d'équipements ? je suis vraiment curieux de savoir comment cela peut arriver... pourquoi vous utilisez un bloc de synchronisation vide : synchronized(this) {} ?
86 votes
Ce n'est même pas sûr du tout.
1 votes
@MattBall, et ceci est un code réduit. Évidemment, l'écriture vers
currentPos
n'a pashappen-before
la lecture de celui-ci, mais je ne vois pas comment cela peut être le problème.1 votes
@MartinV. car c'est la seule façon dont j'ai pu reproduire le comportement. Le fait de verrouiller sur n'importe quoi fait la même chose.
9 votes
Il est intéressant de noter que l'ajout de l'option
final
(qui n'a aucun effet sur le bytecode produit) aux champsx
yy
"résout" le bogue. Bien que cela n'affecte pas le bytecode, les champs sont marqués avec, ce qui me fait penser qu'il s'agit d'un effet secondaire d'une optimisation de la JVM.3 votes
@Dog Le résultat est également très "attendu". Deux Threads indépendants agissant sur des données partagées sans aucun type de synchronisation, il n'y a aucun moyen de savoir le résultat de ceci. Il se peut même que ça ne se termine jamais.
9 votes
@Eugene : Il devrait pas fin. La question est "pourquoi cela se termine-t-il ?". A
Point
p
est construit qui satisfaitp.x+1 == p.y
alors a référence est transmis au fil de sondage. Finalement, le fil d'interrogation décide de se retirer parce qu'il pense que la condition n'est pas satisfaite pour l'un des éléments suivantsPoint
qu'il reçoit, mais la sortie de la console montre ensuite qu'il aurait dû être satisfait. L'absence devolatile
signifie simplement que le fil de sondage peut se bloquer, mais ce n'est manifestement pas le problème ici.2 votes
@ErmaK.Pizarro Si ces deux lignes : if (p.x+1 != p.y) System.out.println(p.x+" "+p.y) ; se suivent, cela ne signifie pas qu'elles sont atomiques. Tout peut se passer entre eux. A moins de savoir ce qu'il veut, il est impossible de répondre
2 votes
@Eugène, je ne pense pas que tu lises le code.
p.x
yp.y
ne peuvent pas être modifiées, elles ne sont définies que dans le constructeur de l'applicationPoint
. La seule façon d'obtenir unPoint
est d'appeler le constructeur qui ne retourne pas jusqu'à ce quep.x
yp.y
sont définis. Il est clair qu'aucun code ici ne modifie un code déjà existant.Point
.2 votes
Je me suis efforcé de simplifier les choses. Voir stackoverflow.com/questions/16178020/
3 votes
MattBall : Vous ne semblez pas connaître grand-chose à la concurrence de Java. Ce programme peut être rendu thread safe simplement en rendant
currentPos
volatile
. Cela garantirait que les écritures dans le constructeurhappen-before
l'écriture decurrentPos
. Une autre solution consiste à faire en sorte que les champs dansPoint
final
qui fonctionnerait grâce à la section sémantique du champ final de JLS. Le bloc synchronisé est simplement un noop que OP a mis pour reproduire le problème. Même sans connaissance du modèle de mémoire Java, la plupart des gens supposeraient que ce programme est thread safe.2 votes
Je suppose que c'est un exemple de la raison pour laquelle les tests sont considérés comme plus que souhaitables.
23 votes
@JohnNicholas : Le vrai code (qui n'est évidemment pas celui-ci) avait une couverture de test de 100% et des milliers de tests, dont beaucoup testaient des choses dans des milliers d'ordres et de permutations différents... Les tests ne trouvent pas par magie tous les cas de figure causés par un JIT/cache/ordonnanceur non déterministe. Le vrai problème est que le développeur qui a écrit ce code ne savait pas que la construction ne se fait pas avant l'utilisation de l'objet. Remarquez comment la suppression de l'objet vide
synchronized
fait que le bug ne se produit pas ? C'est parce que j'ai dû écrire du code au hasard jusqu'à ce que j'en trouve un qui reproduise ce comportement de façon déterministe.1 votes
Cool, tu devrais mettre à jour ta question avec cette info sur la synchronisation :D Je trouve personnellement cette histoire fascinante au fur et à mesure qu'elle se déroule.
0 votes
Dans les cas où une défaillance du logiciel peut provoquer des événements ayant de mauvaises conséquences, vous devez toujours ont une sorte de fonction de surveillance indépendante. Les applications médicales/aérospatiales/automobiles/ferroviaires/nucléaires le savent bien.
4 votes
Pourquoi n'a-t-il pas utilisé un système de verrouillage matériel ? N'avons-nous rien appris depuis 1985 ? (Therac-25) : codingninja.co.uk/trust-me-im-a-programmer
6 votes
@Dog Est-ce que c'est news.nationalpost.com/2013/08/16/ vous ?
0 votes
+1 à Darknight concernant Therac-25. Au moins, cet incident n'a causé que des dommages matériels au lieu de tuer des gens.