Odczytywanie nieaktualnej wartości po odczytaniu nowszej wartości [duplikat]

Nov 24 2020

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 varjest niestabilny. Mam przeczucie, że to niemożliwe.

Odpowiedzi

3 Eugene Nov 26 2020 at 11:41

Używasz System.out.printlntego 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 varsą 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-beforeporzą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 ordermó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 orderi 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 consistencychodzi 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-beforealbo 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 1niż 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 SWdaje, HBponieważ:

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 ordermówi, że wątek odczytu odczyta wartość, która została „zapisana w ostatnim HB”, lub oznacza to, że czytanie 1wtedy 0jest 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ć 1niż 0.

1 akuzminykh Nov 24 2020 at 17:04

Kiedy wartość varzostanie odczytana i 1nie 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 1i 1 1.

Kluczową kwestią, którą należy tutaj zrozumieć, jest printlnsynchronizacja. Zajrzyj do środka tej metody i powinieneś zobaczyć synchronizedtam. 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ść, vara 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ż varnie jest oznaczony, volatileani nie jest w żaden sposób zsynchronizowany.

AlexRevetchi Nov 26 2020 at 15:32

Myślę, że brakuje tutaj faktu, że te wątki działają na rzeczywistych rdzeniach fizycznych i mamy tutaj kilka możliwych wariantów:

  1. 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

  2. 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.

  3. 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.

FrancescoMenzani Nov 25 2020 at 01:35

Dodawanie do innych odpowiedzi:

W przypadku longi doublezapisy 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.