Pourquoi ce programme Java se termine-t-il malgré que apparemment il ne devrait pas (et pas)?

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):

  • à px
  • à py
  • au courant

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:

  1. lit currentpos à p
  2. lire px et py (dans n’importe quel ordre)
  3. comparer et prendre la twig
  4. lisez px et py (n’importe quel ordre) et appelez System.out.println

É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:

  1. 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 n’ait eu lieu – obtient 0. Compare evals à true. Les écritures deviennent visibles pour T. System.out.println est appelé.
  2. Il lit d’abord currentpos.x, après que l’écriture x se soit produite, puis lit currentpos.y avant que y ait été écrit – obtient 0. Compare evals à true. Les écritures deviennent visibles pour T … etc.
  3. Il lit d’abord currentpos.y, avant que y ait été écrit (0), puis lit currentpos.x après l’écriture x, evals à true. etc.

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.

  • Si vous faites en sorte que les champs x, y soient définitifs, Java garantit que les écritures de leurs valeurs apparaîtront avant le retour du constructeur, dans tous les threads. Ainsi, l’affectation à currentpos étant après le constructeur, il est garanti que le thread T voit les écritures dans le bon ordre.
  • Si vous rendez volatil currentpos, alors Java garantit qu’il s’agit d’un sharepoint synchronisation qui sera totalement ordonné par rapport aux autres points de synchronisation. Comme dans la plupart des cas les écritures sur x et y doivent avoir lieu avant l’écriture sur currentpos, alors toute lecture de currentpos dans un autre thread doit aussi voir les écritures de x, y qui se sont produites auparavant.

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:

  1. x = 10, y = 11
  2. thread de travail évalue px comme 10
  3. le thread principal exécute la mise à jour, maintenant x = 11 et y = 12
  4. thread de travail évalue py comme 12
  5. thread de travail remarque que 10 + 1! = 12, donc imprime et quitte.

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.