Lecture d'une valeur périmée après la lecture d'une valeur plus récente [dupliquer]
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 var
n'est pas volatile. Mon instinct est que ce n'est pas possible.
Réponses
Vous utilisez System.out.println
cela 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 var
sont incluses program order
, nous pouvons dessiner:
(po)
firstRead(var) ------> secondRead(var)
// po == program order
Cette phrase dit également que cela construit un happens-before
ordre, 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 order
dit:
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 order
et 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 consistency
qu'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-before
ou toute autre écriture .
Puisqu'il n'y a aucun synchronizes-with order
là, 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 SW
donne HB
parce 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 order
dit que le fil de lecture lira la valeur qui a été "écrite dans le dernier HB", ou cela signifie que la lecture est 1
alors 0
impossible.
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, 0
en conséquence. C'est juste pour prouver que même avec synchronized
(qui vient en interne de System.out.println
), vous pouvez toujours obtenir 1
que 0
.
Lorsque la valeur de var
est lue et qu'elle 1
ne 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 1
et 1 1
.
Le point clé à comprendre ici est qu'il println
s'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 var
mais 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 var
n'est pas marquée volatile
et l'écriture n'est en aucun cas synchronisée.
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:
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
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.
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.
Ajout aux autres réponses:
Avec long
et 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.