Lecture d'une valeur périmée après la lecture d'une valeur plus récente [dupliquer]

Nov 24 2020

Prenons cet exemple. Nous avons:

int var = 0;

Fil A:

System.out.println(var);
System.out.println(var);

Filetage B:

var = 1;

Les threads s'exécutent simultanément. La sortie suivante est-elle possible?

1
0

Autrement dit, la valeur d'origine est lue après la lecture de la nouvelle valeur. Le varn'est pas volatile. Mon instinct est que ce n'est pas possible.

Réponses

3 Eugene Nov 26 2020 at 11:41

Vous utilisez System.out.printlncela en interne synchronized(this) {...}pour aggraver les choses. Mais même avec cela, votre fil de lecture peut toujours observer 1, 0, c'est-à-dire: une lecture racée.

Je ne suis de loin pas un expert en la matière, mais après avoir parcouru de nombreuses vidéos / exemples / blogs d'Alexey Shipilev, je pense comprendre au moins quelque chose.

JLS déclare que:

Si x et y sont des actions du même thread et que x vient avant y dans l'ordre du programme, alors hb (x, y).

Puisque les deux lectures de varsont incluses program order, nous pouvons dessiner:

                (po) 
firstRead(var) ------> secondRead(var)
// po == program order

Cette phrase dit également que cela construit un happens-beforeordre, donc:

                (hb) 
firstRead(var) ------> secondRead(var)
// hb == happens before

Mais c'est dans «le même fil». Si nous voulons raisonner sur plusieurs threads, nous devons examiner l' ordre de synchronisation . Nous en avons besoin parce que le même paragraphe à propos de happens-before orderdit:

Si une action x se synchronise avec une action suivante y, alors nous avons aussi hb (x, y).

Donc, si nous construisons cette chaîne d'actions entre program orderet synchronizes-with order, nous pouvons raisonner sur le résultat. Appliquons cela à votre code:

            (NO SW)                    (hb)
write(var) ---------> firstRead(var) -------> secondRead(var)

// NO SW == there is "no synchronizes-with order" here
// hb    == happens-before

Et c'est là happens-before consistencyqu'intervient dans le même chapitre :

Un ensemble d'actions A se produit-avant cohérent si pour toutes les lectures r dans A, où W (r) est l'action d'écriture vue par r, ce n'est pas le cas que hb (r, W (r)) ou qu'il y ait existe une écriture w dans A telle que wv = rv et hb (W (r), w) et hb (w, r).

Dans un ensemble d'actions cohérent qui se produit avant, chaque lecture voit une écriture qu'il est autorisée à voir par l'ordre des événements avant

J'avoue que je comprends très vaguement la première phrase et c'est là qu'Alexey m'a le plus aidé, comme il le dit:

Les lectures voient la dernière écriture qui s'est produite dans le happens-beforeou toute autre écriture .

Puisqu'il n'y a aucun synchronizes-with orderlà, et implicitement qu'il n'y en a pas happens-before order, le thread de lecture est autorisé à lire via une course. et ainsi obtenir 1, que 0.


Dès que vous introduisez un correct synchronizes-with order, par exemple un à partir d'ici

Une action de déverrouillage sur le moniteur m se synchronise avec toutes les actions de verrouillage suivantes sur ...

Une écriture dans une variable volatile v se synchronise - avec toutes les lectures ultérieures de v par n'importe quel thread ...

Le graphique change (disons que vous avez choisi de faire var volatile):

               SW                       PO
write(var) ---------> firstRead(var) -------> secondRead(var)

// SW == there IS "synchronizes-with order" here
// PO == happens-before

PO(ordre du programme) donne cela HB(se produit avant) via la première phrase que j'ai citée dans cette réponse du JLS. Et SWdonne HBparce que:

Si une action x se synchronise avec une action suivante y, alors nous avons aussi hb (x, y).

En tant que tel:

               HB                       HB
write(var) ---------> firstRead(var) -------> secondRead(var)

Et maintenant happens-before orderdit que le fil de lecture lira la valeur qui a été "écrite dans le dernier HB", ou cela signifie que la lecture est 1alors 0impossible.


J'ai pris l'exemple des échantillons jcstress et j'ai introduit un petit changement (tout comme le vôtre System.out.println):

@JCStressTest
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "Doing both reads early.")
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "Doing both reads late.")
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "Doing first read early, not surprising.")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "First read seen racy value early, and the second one did not.")
@State
public class SO64983578 {

    private final Holder h1 = new Holder();
    private final Holder h2 = h1;

    private static class Holder {

        int a;
        int trap;
    }

    @Actor
    public void actor1() {
        h1.a = 1;
    }

    @Actor
    public void actor2(II_Result r) {
        Holder h1 = this.h1;
        Holder h2 = this.h2;
        
        h1.trap = 0;
        h2.trap = 0;

        synchronized (this) {
            r.r1 = h1.a;
        }

        synchronized (this) {
            r.r2 = h2.a;
        }

    }

}

Notez synchronized(this){....}que cela ne fait pas partie de l'exemple initial. Même avec la synchronisation, je peux toujours voir cela 1, 0en conséquence. C'est juste pour prouver que même avec synchronized(qui vient en interne de System.out.println), vous pouvez toujours obtenir 1que 0.

1 akuzminykh Nov 24 2020 at 17:04

Lorsque la valeur de varest lue et qu'elle 1ne change pas. Cette sortie ne peut pas se produire, ni en raison de la visibilité ni des réorganisations. Ce qui peut arriver, c'est 0 0, 0 1et 1 1.

Le point clé à comprendre ici est qu'il printlns'agit d'une synchronisation. Regardez à l'intérieur de cette méthode et vous devriez en voir un synchronized. Ces blocs ont pour effet que les impressions se produiront dans cet ordre. Bien que l'écriture puisse avoir lieu à tout moment, il n'est pas possible que la première impression voit la nouvelle valeur de varmais que la deuxième impression voit l'ancienne valeur. Par conséquent, l'écriture ne peut avoir lieu qu'avant les deux impressions, entre elles ou après celles-ci.

En outre, il n'y a aucune garantie que l'écriture sera visible du tout, car elle varn'est pas marquée volatileet l'écriture n'est en aucun cas synchronisée.

AlexRevetchi Nov 26 2020 at 15:32

Je pense que ce qui manque ici, c'est le fait que ces threads fonctionnent sur des cœurs physiques réels et que nous avons quelques variantes possibles ici:

  1. tous les threads fonctionnent sur le même noyau, alors le problème est réduit à l'ordre d'exécution de ces 3 instructions, dans ce cas 1,0 n'est pas possible je pense, les exécutions println sont ordonnées en raison des barrières mémoire créées par la synchronisation, de sorte que exclut 1,0

  2. A et B fonctionnent sur 2 cœurs différents, puis 1,0 ne semble pas possible non plus, dès que le noyau qui exécute le thread A lit 1, il n'y a aucun moyen qu'il lira 0 après, comme ci-dessus, les printlns sont ordonnés.

  3. Le thread A est replanifié entre ces 2 printlns, donc le second println est exécuté sur un noyau différent, soit le même que B était / sera exécuté, soit sur un 3ème noyau différent. Ainsi, lorsque les 2 printlns sont exécutés sur des cœurs différents, cela dépend de la valeur que les 2 cœurs voient, si var n'est pas synchronisé (il n'est pas clair que var en fait partie), alors ces 2 cœurs peuvent voir une valeur de var différente, donc il y a une possibilité pour 1,0.

C'est donc un problème de cohérence du cache.

PS Je ne suis pas un expert jvm, donc il y a peut-être d'autres choses en jeu ici.

FrancescoMenzani Nov 25 2020 at 01:35

Ajout aux autres réponses:

Avec longet double, les écritures peuvent ne pas être atomiques, donc les 32 premiers bits peuvent devenir visibles avant les 32 derniers bits, ou vice versa. Par conséquent, des valeurs complètement différentes peuvent être émises.