web-dev-qa-db-fra.com

Pourquoi ce programme Java se termine-t-il malgré le fait qu'il ne devrait pas (et ne l'a pas) apparemment?

Une opération délicate dans mon laboratoire aujourd'hui s'est complètement déroulée. Un actionneur au microscope électronique a franchi ses limites et, après une série d'événements, j'ai perdu 12 millions de dollars d'équipement. J'ai réduit au-dessus 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 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

Puisqu'il n'y a pas d'arithmétique en virgule flottante ici, et que nous savons tous que les entiers signés se comportent bien en débordement en Java, je penserais 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 sur un Linux 64 bits.

205
Dog

De toute évidence, l'écriture sur currentPos ne se produit pas avant la lecture de celle-ci, mais je ne vois pas en quoi cela pourrait être le problème.

currentPos = new Point(currentPos.x+1, currentPos.y+1); fait plusieurs choses, notamment écrire les valeurs par défaut dans x et y (0), puis écrire leurs valeurs initiales dans le constructeur. Puisque 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.

Donc, du point de vue du fil de lecture, il est légal 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 objet est effectivement immuable - si dans votre cas d'utilisation réel l'objet est muté après la construction, _ les garanties volatile ne suffiront pas et vous pourriez voir à nouveau un objet incohérent.

Vous pouvez également rendre la variable Point immuable, ce qui garantira également une publication sûre, même sans utiliser volatile. Pour obtenir l'immutabilité, il vous suffit de marquer x et y final.

En guise de remarque, et comme déjà mentionné, synchronized(this) {} peut être traité comme un non-fonctionnement par la JVM (si j'ai bien compris, vous l'avez inclus pour reproduire le problème).

140
assylias

Puisque currentPos est en cours de modification en dehors du fil, il doit être marqué comme suit: volatile:

static volatile Point currentPos = new Point(1,2);

Sans volatile, il n'est pas garanti que le thread lise les mises à jour de currentPos effectuées dans le thread principal. Donc, de nouvelles valeurs continuent à être écrites pour currentPos mais le thread continue à utiliser les versions en cache précédentes pour des raisons de performances. Comme un seul thread modifie currentPos, vous pouvez vous en sortir sans verrou, ce qui améliorera les performances.

Les résultats semblent très différents si vous ne lisez les valeurs qu’une seule fois dans le fil pour les utiliser dans la comparaison et leur affichage ultérieur. Lorsque je fais ce qui suit x s'affiche toujours sous la forme 1 et y varie entre 0 et un grand 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 contribue. De même, si je commente le bloc vide synchronized(this) {}, le code fonctionne également et je suppose que c'est parce que le verrouillage provoque un délai suffisant pour que currentPos et ses champs soient relus plutôt qu'utilisés à partir du cache.

int x = p.x + 1;
int y = p.y;

if (x != y) {
    System.out.println(x+" "+y);
    System.exit(1);
}
29
Ed Plese

Vous avez la mémoire ordinaire, la référence 'currentpos' et l'objet Point et ses champs derrière, partagés entre 2 threads, sans synchronisation. Ainsi, il n'y a pas d'ordre 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 de point, les valeurs par défaut de p.x et de p.y seront obtenues):

  • à p.x
  • pour p.y
  • to currentpos

Parce qu’il n’ya rien de spécial dans ces écritures 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) à tout moment entre les lectures dans T.

Donc T fait:

  1. lit currentpos à p
  2. lire p.x et p.y (dans l'un ou l'autre ordre)
  3. comparer et prendre la branche
  4. lisez p.x et p.y (soit l'ordre) et appelez System.out.println

Etant donné qu'il n'y a pas de relation d'ordre entre les écritures dans main et les lectures dans T, il existe clairement plusieurs façons dont cela peut produire votre résultat, car T peut voir l'écriture de main dans currentpos avant les écritures sur currentpos.y ou currentpos.x:

  1. Il commence par lire currentpos.x, avant que x n’ait eu lieu - obtient 0, puis lit currentpos.y avant que y ne se produise - obtient 0. Compare evals à true. Les écritures deviennent visibles pour T. System.out.println est appelé.
  2. Il commence par lire currentpos.x, après l’écriture x, puis currentpos.y avant l’écriture y - obtient 0. Compare evals à true. Les écrits deviennent visibles pour T ... etc.
  3. Il commence par lire currentpos.y, avant l’écriture y (0), puis lit currentpos.x après l’écriture x, valant 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 consiste à penser que les écritures résultant de cette ligne sont visibles sur tous les threads dans l'ordre des programmes du thread qui l'exécute:

currentPos = new Point(currentPos.x+1, currentPos.y+1);

Java ne fait aucune telle garantie (ce serait terrible pour la performance). Quelque chose de plus doit être ajouté si votre programme a besoin d'un ordre garanti des écritures par rapport aux lectures d'autres threads. D'autres ont suggéré de rendre les champs x, y finaux ou, au contraire, de rendre currentpos volatil.

  • Si vous définissez les champs x, y comme finaux, alors Java garantit que les écritures de leurs valeurs se produiront avant le retour du constructeur, dans tous les threads. Ainsi, comme l'affectation à currentpos est postérieure au constructeur, le thread T est assuré de voir les écritures dans le bon ordre.
  • Si vous rendez currentpos volatile, alors Java garantit qu'il s'agit d'un point de synchronisation qui sera ordonné au total par rapport à d'autres points de synchronisation. Comme en général, les écritures sur x et y doivent avoir lieu avant l'écriture sur currentpos, toute lecture de currentpos dans un autre thread doit également voir les écritures de x, y qui se sont produites auparavant.

L'utilisation de final a pour avantage de rendre les champs immuables et de permettre ainsi la mise en cache des valeurs. L'utilisation de fonctions volatiles entraîne une synchronisation à chaque écriture et lecture de currentpos, ce qui pourrait nuire aux performances.

Reportez-vous au chapitre 17 de la spécification de langage Java pour plus de détails: 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 garantie volatile de JLS soit suffisante. Réponse modifiée pour refléter les commentaires d’assylias, soulignant que le modèle Java est plus fort - se passe avant, est transitif - et tellement volatile. on currentpos suffit également).

19
paulj