Java concurrency in practice - volatile, immutable, safe publication

0

Cześć, nie mogę ogarnąć jednej rzeczy z w/w książki. Pytanie raczej do tych co czytali.
Podrozdziały od 3.4.1 do 3.5.2 włącznie.

Jest sobie przykład mówiący, że jeśli niezmienniki zamkniemy w niemutowalnej klasie to można potem tego bezpiecznie użyć bez synchronizacji:

class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {
        this.lastNumber = lastNumber;
        this.lastFactors = Arrays.copyOf(lastFactors, lastFactors.length);
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i)) {
            return null;
        }
        return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

class VolatileCachedFactorized implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);

        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }

        encodeIntoResponse(resp, factors);
    }
}

Na pewno client nie zobaczy niespójnego stanu, ale mimo wszystko mamy tutaj check-then-act no nie? Jest warunek if na factors a potem przypisanie nowej referencji do cache. Teoretycznie nie ma to żadnych konsekwencji w sumie.
Zastanawiam się jednak czy dobrze rozumiem to volatile tutaj. Jak rozumiem jest ono użyte po to, aby wątki widziały jak najnowszą referencję tutaj tak? Bez tego jakiś wątek mógłby nie widzieć najnowszego nadpisania. W tym przypadku akurat też za dużo to nie rozwali co nie?

No właśnie .. a potem jest przykład z nieprawidłową publikacją:

class Holder {

    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n) {
            throw new AssertionError("This statement is false");
        }
    }
}

class Foo {

    public Holder holder;

    public void init() {
        holder = new Holder(42);
    }
}

No i tutaj jest jasne, że po 1 jest mutowalne, po 2 może nie widzieć najświeższej referencji do holdera albo widzieć go w złym stanie.

Jednak potem jest informacja, że gdyby Holder był niemutowalny, to można go użyć bez żadnej synchronizacji.

Czyli jak rozumiem jeśli jakaś zmienna w klasie jest finalna to nie może dojść sytuacji, że wątek zostanie wywłaszczony pomiędzy super-konstruktorem Object, a tym konkretnym? I to jest już sprawa JMM? Kiedy mam pewność, że mój konstruktor wykona się na 100% w całości? Kiedy wszystkie pola mam finalne czy wystarczy tylko 1?

I ostatnia sprawa związana już konkretnie z tą publikacją niemutowalnego obiektu. W końcu referencja na niemutowalny obiekt musi być volatile czy nie? Bo w przykładzie referencja na OneCacheValue jest volatile, ale potem jest napisane:

Immutable objects can be used safely by any thread without additional synchronization, even when synchronization is not used to publish them.

To jak to w końcu o_O?

1
Bambo napisał(a):

I ostatnia sprawa związana już konkretnie z tą publikacją niemutowalnego obiektu. W końcu referencja na niemutowalny obiekt musi być volatile czy nie? Bo w przykładzie referencja na OneCacheValue jest volatile, ale potem jest napisane:

Immutable objects can be used safely by any thread without additional synchronization, even when synchronization is not used to publish them.

To jak to w końcu o_O?

Tylko że volatile nie załatwia problemu synchronizacji - nadal, pomimo że wątek już sobie nie robi lokalnego cache'u, potrzebna jest synchronizacja, bo jest możliwy race condition.

0

@Pinek: no ale tutaj nie ma race condition, które Ci coś zepsuje. Stan masz zawsze spójny, bo OneValueCache jest niemutowalny. Jedynie co się źle może stać to, że jeśli np. wątki A i B w takiej kolejności uderzyły do cache i nastąpił jakiś race condition to w cachu nie będę wartości z wątku B tylko A - ale tutaj to nie psuje logiki, niezmienniki są zachowane.

1

Na pewno client nie zobaczy niespójnego stanu, ale mimo wszystko mamy tutaj check-then-act no nie?

Tak, tylko co z tego? ;) Bez volatile mogłoby się okazać, ze referencja jest faktycznie nullem, a ty nigdy nie wchodzisz w tego ifa, bo lokalnie masz cachowaną poprzednią wartość. Ale volatile oczywiście że nie zapewnia to żadnego atomowego update, do tego trzeba by mieć jakieś AtomicReference które robi compare-and-swap i masz pewność, że update tego pola dzieje się atomowo i jeden wątek nigdy nie nadpisze zmiany innego wątku.

Jednak potem jest informacja, że gdyby Holder był niemutowalny, to można go użyć bez żadnej synchronizacji.

Tutaj chodzi o kwestię spójności, tzn jak obiekt jest niemutowalny to nie ma ryzyka że wątek zacznie z nim pracować, cośtam w środku pomiesza, potem inny wątek zacznie pracować na takim w połowie-zmienionym obiekcie. W efekcie obiekty niemutowalne są z definicji thread-safe. Nie musisz się wtedy martwić gdzie taki obiekt przekazujesz ani co ktoś z nim robi.

1
Bambo napisał(a):

Zastanawiam się jednak czy dobrze rozumiem to volatile tutaj. Jak rozumiem jest ono użyte po to, aby wątki widziały jak najnowszą referencję tutaj tak? Bez tego jakiś wątek mógłby nie widzieć najnowszego nadpisania. W tym przypadku akurat też za dużo to nie rozwali co nie?

Tak. Rozwali dużo, bo może się okazać, że nigdy pozostałe wątki, nie zobaczą nowej wartości (np przez cały czas działania aplikacji wartość siedzi w L1 procesora) i w zasadzie cache nie działa w ogóle

Czyli jak rozumiem jeśli jakaś zmienna w klasie jest finalna to nie może dojść sytuacji, że wątek zostanie wywłaszczony pomiędzy super-konstruktorem Object, a tym konkretnym? I to jest już sprawa JMM? Kiedy mam pewność, że mój konstruktor wykona się na 100% w całości? Kiedy wszystkie pola mam finalne czy wystarczy tylko 1?

To nie kwestia konstruktora, tutaj Twój case: https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5

I ostatnia sprawa związana już konkretnie z tą publikacją niemutowalnego obiektu. W końcu referencja na niemutowalny obiekt musi być volatile czy nie? Bo w przykładzie referencja na OneCacheValue jest volatile, ale potem jest napisane:
Immutable objects can be used safely by any thread without additional synchronization, even when synchronization is not used to publish them.
To jak to w końcu o_O?

Tutaj dwie sprawy mieszasz ze sobą. Publikacja niemutowalnego obiektu jest bardzo prosta, wystarczy go stworzyć:

class Person {
     final String name;
     // konstruktor
}  

Person p = new Person("name"); // ten obiekt jest thread safe a JMM gwarantuje, że jeśli przekażemy go do innych wątków, to każdy z nich będzie widział zainicjalizowany String name

Natomiast gdybyśmy chcieli, żeby referencja p po jakimś tam czasie, zaczeła wskazywać na inny obiekt, to wtedy powinniśmy oznaczyć ja volatile, żeby pozostałe wątki zostały o tym poinformowane. Więc to są 2 różne rzeczy, które często się zresztą stosuje razem.

1

W Javie pola finalne są o tyle specjalne, że powodują dodanie memory barrier po ich inicjalizacji. W skrócie, jeśli któryś wątek zobaczy referencję do obiektu (volatile zapewnia właśnie to, że wątek czytający referencję będzie widział jej "last write"), to ma też gwarancję, że pola finalne do których może się dostać używając tej referencji będą zainicjalizowane.

I ostatnia sprawa związana już konkretnie z tą publikacją niemutowalnego obiektu. W końcu referencja na niemutowalny obiekt musi być volatile czy nie?

Zapis do referencji jest zawsze atomowy, więc wątek T1 może zapisać coś do referencji a i wątek T2 może odczytać tą referencję i widzieć tam non-null. W tym przypadku wszystko zadziała tak jakby był użyty volatile. Problem w tym, że wątek T2 może też nigdy zapisu wykonanego przez T1 nie widzieć, po prostu nie ma takiej gwarancji jeśli kod nie jest synchronizowany (może tam widzieć np. zawsze nulla). Volatile jest potrzebny po to, żeby upewnić się, że wątek który czyta referencję widzi ostatni zapis do tej referencji wykonany przez jakikolwiek inny wątek.

Dlatego kwestia widzenia samej referencji, a widzenia w pełni zainicjalizowanego obiektu to trochę dwie osobne kwestie :)

0

@Shalom: Która referencja może być volatile nullem? Do OneValueCache? Wtedy chyba po prostu poleci NPE ? I drugie pytanie .. dlaczego mam nie móc wejść do ifa nigdy? Co się może takiego stać?

@pedegie Czegoś nie czaję. Utworzyłeś, czyli opublikowałeś obiekt Person i on jest wszędzie widoczny, ale jak opublikujesz nowy obiekt Person i przypiszesz do p to już nie będzie widoczny bez volatile?, czyli:

Person p = new Person("Name1"); // widoczne

// later

p = new Person("Name2"); // niewidoczne?

:O

4

@Bambo: Na przykładzie klasy Point

class Point {
    int x;
    int y;
    // konstruktor
}

Point point = new Point(5, 10);
new Thread(() -> {
      int xThread = point.x;
}.start()

W uproszczeniu są tutaj 4 operacje:

x = 5
y = 10
point = adres
xThread = point.x

Teraz nic nie stoi na przeszkodzie, żeby kompilator lub CPU zmienił kolejność tych instrukcji np na taką:

point = adres
y = 10
xThread = point.x
x = 5

żeby uniknąc takich sytuacji, twórcy kompilatorów Jav'y są zobowiązani (od 1.5) żeby przestrzegać tych dwóch reguł:

  1. A store of a final field (inside a constructor) and, if the field is a reference, any store that this final can reference, cannot be reordered with a subsequent store (outside that constructor) of the reference to the object holding that field into a variable accessible to other threads. For example, you cannot reorder
    x.finalField = v; ... ; sharedRef = x;
    This comes into play for example when inlining constructors, where "..." spans the logical end of the constructor. You cannot move stores of finals within constructors down below a store outside of the constructor that might make the object visible to other threads. (As seen below, this may also require issuing a barrier). Similarly, you cannot reorder either of the first two with the third assignment in:
    v.afield = 1; x.finalField = v; ... ; sharedRef = x;
  1. The initial load (i.e., the very first encounter by a thread) of a final field cannot be reordered with the initial load of the reference to the object containing the final field. This comes into play in:
    x = sharedRef; ... ; i = x.finalField;
    A compiler would never reorder these since they are dependent, but there can be consequences of this rule on some processors.

Źródło: http://gee.cs.oswego.edu/dl/jmm/cookbook.html

Wracając do przykładu, jeśli któreś z pól jest oznaczone jako final w konstruktorze, to mamy gwarancję, że zostanie ono przypisane do zmiennej przed przypisaniem point = adres.

Point point = new Point(final 5, 10); // pseudokod
final x = 5
point = adres 

Oznaczając x final mamy gwarancje, że ta kolejność zostanie zachowana, z kolei non-final y mógłby być w każdym miejscu, tj przed przypisaniem do x, pomiędzy przypisaniami lub po przypisaniu point.

2

Jeszcze dodam, że nie należy się sugerować przy programowaniu wielowątkowym że "program mi działa" bo np. na Intelu cache jest koherentny, więc generalnie jeśli tylko CPU faktycznie dostanie do wykonania zapis wartości pod wskazany adres i zrealizuje te instrukcje to inny wątek dostanie ostatnią wartość. Na Intelu tak jakby każda wartość była volatile... co potrafi ukryć całkiem sporo błędów.

Ale oczywiście bez volatile w kodzie kompilator ma pełną wolność umieścić zmienną w rejestrze i w ogóle nie wygenerować przesłania do pamięci, albo wygenerować je ale dużo później, i mimo koherentnego cache będziemy mieć problem.

No i już na ARMach tak dobrze nie jest.

1 użytkowników online, w tym zalogowanych: 0, gości: 1