Une opération délicate dans mon laboratoire aujourd’hui s’est complètement trompée. Un actionneur sur un microscope électronique a dépassé ses limites et, après une série d’événements, j’ai perdu 12 millions de dollars d’équipement. J’ai réduit les lignes de plus de 40K 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(Ssortingng[] args) { new Thread() { void f(Point p) { synchronized(this) {} if (p.x+1 != py) { System.out.println(p.x+" "+py); 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 exemples 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
Comme il n’y a pas d’arithmétique à virgule flottante ici et que nous soaps 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 avec 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 a été atteint et non atteint?). Pourquoi?
J’ai remarqué que cela ne se produit pas dans certains environnements. Je suis sur OpenJDK 6 sous Linux 64 bits.
De toute évidence, l’écriture sur currentPos n’a pas lieu avant sa lecture, mais je ne vois pas comment cela peut être le problème.
currentPos = new Point(currentPos.x+1, currentPos.y+1);
fait quelques choses, y compris écrire les valeurs par défaut à x
et y
(0) et ensuite écrire leurs valeurs initiales dans le constructeur. Puisque votre object n’est pas publié en toute sécurité, ces 4 opérations d’écriture peuvent être librement réorganisées par le compilateur / JVM.
Donc, du sharepoint vue du fil de lecture, il s’agit d’une exécution légale de lire x
avec sa nouvelle valeur mais y
avec sa valeur par défaut de 0, par exemple. Lorsque vous atteignez l’instruction println
(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
comme volatile
assurera une publication sûre puisque votre object est effectivement immuable – si, dans votre cas réel, l’object est muté après la construction, volatile
garanties volatile
ne suffiront plus et vous pourriez voir à nouveau un object incohérent.
Alternativement, vous pouvez rendre le Point
immuable qui assurera également la publication en toute sécurité, même sans utiliser volatile
. Pour atteindre l’immuabilité, il vous suffit de marquer x
et y
final.
En tant que remarque complémentaire et comme déjà mentionné, la synchronized(this) {}
peut être traitée comme une no-op par la JVM (si j’ai bien compris, vous l’avez incluse pour reproduire le comportement).
Étant donné que currentPos
est modifié en dehors du thread, il devrait être marqué comme volatile
:
static volatile Point currentPos = new Point(1,2);
Sans volatilité, il est impossible de lire le thread dans les mises à jour de currentPos effectuées dans le thread principal. Les nouvelles valeurs continuent donc à être écrites pour currentPos, mais les threads continuent à utiliser les versions antérieures mises en cache pour des raisons de performances. Comme un seul thread modifie le paramètre currentPos, vous pouvez vous en sortir sans verrou, ce qui améliorera les performances.
Les résultats sont très différents si vous lisez les valeurs une seule fois dans le thread pour les utiliser lors de la comparaison et de l’affichage ultérieur. Lorsque je le fais, x
affiche toujours 1
et y
varie entre 0
et un grand nombre entier. Je pense que son comportement à ce stade est quelque peu indéfini sans le mot-clé volatile
et il est possible que la compilation JIT du code y consortingbue. Aussi, si je commente le bloc vide synchronized(this) {}
, le code fonctionne aussi bien et je pense que c’est parce que le locking retarde suffisamment que currentPos
et ses champs soient relus au lieu d’être utilisés depuis le cache.
int x = px + 1; int y = py; if (x != y) { System.out.println(x+" "+y); System.exit(1); }
Vous avez une mémoire ordinaire, la référence ‘currentpos’ et l’object Point et ses champs derrière elle, partagés entre 2 threads, sans synchronisation. Ainsi, il n’y a pas d’ordonnancement défini entre les écritures qui arrivent dans 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 du point, il en résultera que px et py auront des valeurs par défaut):
Comme il n’y a rien de particulier à propos de ces écritures en termes de synchronisation / barrières, le moteur d’exécution est libre d’autoriser le thread T à les voir apparaître dans n’importe quel ordre (le thread principal voit toujours les écritures et les lectures ordonnées) et se produit à tout moment entre les lectures dans T.
Donc T fait:
Étant donné qu’il n’y a pas de relations d’ordre entre les écritures dans main et les lectures dans T, il y a plusieurs façons de produire votre résultat, T pouvant voir l’écriture principale dans currentpos avant les écritures sur currentpos.y ou currentpos.x:
et ainsi de suite … Il existe un certain nombre de courses de données ici.
Je suppose que l’hypothèse erronée ici suppose que les écritures qui résultent de cette ligne sont visibles à travers tous les threads dans l’ordre de programme du thread qui l’exécute:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
Java ne donne aucune garantie de ce type (ce serait terrible pour la performance). Il faut append quelque chose si votre programme a besoin d’un ordre garanti des écritures par rapport aux lectures dans les autres threads. D’autres ont suggéré de rendre les champs x, y définitifs, ou de rendre le currentpos volatil.
Utiliser final a l’avantage de rendre les champs immuables et donc de mettre les valeurs en cache. 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 Java Language pour les détails gory: 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 JLS était suffisant. Réponse modifiée pour refléter les commentaires des assylias, soulignant que le modèle Java est plus fort ).
Vous pouvez utiliser un object pour synchroniser les écritures et les lectures. Sinon, comme d’autres l’ont déjà dit, une écriture sur currentPos se produira au milieu des deux lectures p.x + 1 et py
new Thread() { void f(Point p) { if (p.x+1 != py) { System.out.println(p.x+" "+py); System.exit(1); } } @Override public void run() { while (currentPos == null); while (true) f(currentPos); } }.start(); Object sem = new Object(); while (true) { synchronized(sem) { currentPos = new Point(currentPos.x+1, currentPos.y+1); } }
Vous accédez à currentPos deux fois et ne garantissez pas qu’il n’est pas mis à jour entre ces deux access.
Par exemple:
Vous comparez essentiellement deux points différents .
Notez que même rendre volatilePost volatile ne vous protégera pas de cela, car il s’agit de deux lectures distinctes par le thread de travail.
Ajouter un
boolean IsValid() { return x+1 == y; }
méthode à votre classe de points. Cela garantira qu’une seule valeur de currentPos est utilisée lors de la vérification de x + 1 == y.