Odczytywanie nieaktualnej wartości po odczytaniu nowszej wartości [duplikat]
Rozważmy ten przykład. Mamy:
int var = 0;
Wątek A:
System.out.println(var);
System.out.println(var);
Wątek B:
var = 1;
Wątki działają jednocześnie. Czy możliwy jest następujący wynik?
1
0
Oznacza to, że oryginalna wartość jest odczytywana po odczytaniu nowej wartości. Nie var
jest niestabilny. Mam przeczucie, że to niemożliwe.
Odpowiedzi
Używasz System.out.println
tego wewnętrznie, synchronized(this) {...}
co może pogorszyć sytuację. Ale nawet z tym, twój wątek czytelnika może nadal obserwować 1, 0
, tj: racy read.
Zdecydowanie nie jestem w tym ekspertem, ale po przejrzeniu wielu filmów / przykładów / blogów Aleksieja Shipileva wydaje mi się, że przynajmniej coś rozumiem.
JLS stwierdza, że:
Jeśli x i y są akcjami tego samego wątku, a x występuje przed y w kolejności programu, to hb (x, y).
Ponieważ oba odczyty var
są dostępne program order
, możemy narysować:
(po)
firstRead(var) ------> secondRead(var)
// po == program order
To zdanie mówi również, że to buduje happens-before
porządek, więc:
(hb)
firstRead(var) ------> secondRead(var)
// hb == happens before
Ale to jest w „tym samym wątku”. Jeśli chcemy uzasadnić wiele wątków, musimy przyjrzeć się kolejności synchronizacji . Potrzebujemy tego, ponieważ ten sam akapit o happens-before order
mówi:
Jeśli akcja x synchronizuje się z następującą po niej akcją y, to mamy również hb (x, y).
Więc jeśli zbudujemy ten łańcuch działań pomiędzy program order
i synchronizes-with order
, możemy wnioskować o wyniku. Zastosujmy to do twojego kodu:
(NO SW) (hb)
write(var) ---------> firstRead(var) -------> secondRead(var)
// NO SW == there is "no synchronizes-with order" here
// hb == happens-before
I o to właśnie happens-before consistency
chodzi w tym samym rozdziale :
Zbiór akcji A występuje - zanim jest spójny, jeśli dla wszystkich odczytów r w A, gdzie W (r) jest akcją zapisu widzianą przez r, nie jest tak, że albo hb (r, W (r)), albo tam istnieje zapis w w A taki, że wv = rv i hb (W (r), w) i hb (w, r).
W spójnym zestawie działań zdarza się przed każdym odczytem widzi zapis, który jest dozwolony przez zdarzenie przed złożeniem zamówienia
Przyznaję, że bardzo niejasno rozumiem pierwsze zdanie i właśnie tu Aleksiej pomógł mi najbardziej, jak to ujmuje:
Odczytuje albo zobaczyć ostatni zapis, który miał miejsce podczas zapisu,
happens-before
albo jakikolwiek inny zapis .
Ponieważ nie ma synchronizes-with order
, i domyślnie nie ma happens-before order
, wątek odczytu może czytać za pośrednictwem wyścigu. iw ten sposób uzyskaj 1
niż 0
.
Jak tylko wprowadzisz poprawny synchronizes-with order
, na przykład ten stąd
Akcja odblokowania na monitorze m synchronizuje się ze wszystkimi kolejnymi akcjami blokowania na ...
Zapis do zmiennej nietrwałej v synchronizuje się ze wszystkimi kolejnymi odczytami v przez dowolny wątek ...
Wykres się zmienia (powiedzmy, że wybrałeś var
volatile
):
SW PO
write(var) ---------> firstRead(var) -------> secondRead(var)
// SW == there IS "synchronizes-with order" here
// PO == happens-before
PO
(kolejność programu) daje to HB
(dzieje się wcześniej) poprzez pierwsze zdanie, które zacytowałem w tej odpowiedzi z JLS. I SW
daje, HB
ponieważ:
Jeśli akcja x synchronizuje się z następującą po niej akcją y, to mamy również hb (x, y).
Takie jak:
HB HB
write(var) ---------> firstRead(var) -------> secondRead(var)
A teraz happens-before order
mówi, że wątek odczytu odczyta wartość, która została „zapisana w ostatnim HB”, lub oznacza to, że czytanie 1
wtedy 0
jest niemożliwe.
Wziąłem przykładowe próbki jcstress i wprowadziłem małą zmianę (tak jak ty 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;
}
}
}
Zwróć uwagę, synchronized(this){....}
że to nie jest częścią początkowego przykładu. Nawet po synchronizacji wciąż to widzę 1, 0
. Ma to na celu udowodnienie, że nawet z synchronized
(to pochodzi wewnętrznie System.out.println
), nadal możesz uzyskać 1
niż 0
.
Kiedy wartość var
zostanie odczytana i 1
nie zmieni się z powrotem. Ten wynik nie może się wydarzyć, ani ze względu na widoczność, ani zmiany kolejności. Co może się zdarzyć 0 0
, 0 1
i 1 1
.
Kluczową kwestią, którą należy tutaj zrozumieć, jest println
synchronizacja. Zajrzyj do środka tej metody i powinieneś zobaczyć synchronized
tam. Te bloki powodują, że wydruki będą się pojawiać właśnie w tej kolejności. Chociaż zapis może nastąpić w dowolnym momencie, nie jest możliwe, aby pierwszy wydruk zobaczył nową wartość, var
a drugi wydruk zobaczył starą wartość. Dlatego zapis może nastąpić tylko przed obydwoma wydrukami, pomiędzy nimi lub po nich.
Poza tym nie ma gwarancji, że zapis będzie w ogóle widoczny, ponieważ var
nie jest oznaczony, volatile
ani nie jest w żaden sposób zsynchronizowany.
Myślę, że brakuje tutaj faktu, że te wątki działają na rzeczywistych rdzeniach fizycznych i mamy tutaj kilka możliwych wariantów:
wszystkie wątki działają na tym samym rdzeniu, wtedy problem sprowadza się do kolejności wykonywania tych 3 instrukcji, w tym przypadku 1,0 nie jest możliwe Myślę, że wykonania println są uporządkowane ze względu na bariery pamięciowe utworzone przez synchronizację, tak że wyklucza 1,0
A i B działają na 2 różnych rdzeniach, wtedy 1,0 też nie wygląda na możliwe, jak tylko rdzeń obsługujący wątek A odczyta 1, nie ma możliwości, aby odczytał po 0, tak samo jak powyżej printlns są zamawiane.
Wątek A jest przełożony pomiędzy tymi 2 wydrukami, więc drugi println jest wykonywany na innym rdzeniu, albo tym samym, co B był / będzie wykonywany, albo na innym trzecim rdzeniu. Więc kiedy 2 printlns są wykonywane na różnych rdzeniach, to zależy, jaką wartość widzą 2 rdzenie, jeśli var nie jest zsynchronizowany (nie jest jasne, czy jest to zmienny), wtedy te 2 rdzenie mogą widzieć inną wartość var, więc istnieje możliwość za 1,0.
Jest to więc problem spójności pamięci podręcznej.
PS Nie jestem ekspertem od jvm, więc mogą tu być inne rzeczy.
Dodawanie do innych odpowiedzi:
W przypadku long
i double
zapisy mogą nie być atomowe, więc pierwsze 32 bity mogą stać się widoczne przed ostatnimi 32 bitami lub odwrotnie. Dlatego można wyprowadzić zupełnie inne wartości.