Safe publication - czy dużo się zmieniło od legendarnej książki?

Safe publication - czy dużo się zmieniło od legendarnej książki?
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

Cześć, po przeczytaniu "Java concurrency in Practice" bawię kilkoma rzeczami, które miałem potrzebę samemu sprawdzić eksperymentalnie.

Przede wszystkim najmocniej analizowałem safe publication podczas czytania.
Wtajemniczeni kojarzą być może taki obiekt:

Kopiuj
public 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.");
    }
}

Napisałem sobie jakiegoś callera:

Kopiuj
public class Main {

    private Holder holder;

    void execute() throws InterruptedException {
        final ExecutorService executorService = Executors.newFixedThreadPool(100);

        final Thread thread = new Thread(() -> holder = new Holder(1000));
//        thread.start(); // A
//        thread.join(); // A+B
        holder = new Holder(1000); // C

        for (int i = 0; i < 1000; ++i) {
            executorService.execute(() -> holder.assertSanity());
        }

        executorService.shutdown();
    }

    public static void main(String[] args) throws InterruptedException {
        new Main().execute();
    }
}

I teraz tak ...
A - mamy unsafe publication, bo przetestowałem i czasami leci NPE na holderze jak executor w innych wątkach odpala na nim assertSanity. Nigdy jednak nie udało mi się dostać wyjątku AssertionError czyli, że referencja do holdera była, ale stan był niespójny. Być może po prostu taka sytuacja na nowych javach ma miejsce wyjątkowo rzadko?

A + B - tutaj nie udało mi się wywalić niczego, zawsze działa. Dlaczego? Przecież wątek callera to inny wątek niż te w executorze i mimo, że czekam aż się ten wątek inicjujący holdera wykona to one zawsze go widzą? To znowu efekt super nowych JDK?

C - tutaj podobnie jak w A+B, nie udało mi się wywalić i pewnie sprawa jest podobna jak w pkt poprzednim?

Pytanie bonus ...
Skoro konstruktory nie są synchronizowane to jeśli tworzymy obiekt, który będzie zaraz czytany przez wiele innych wątków to czy referencje nie powinna być zawsze final lub volatile? Dodatkowo jeśli ten obiekt jest mutowalny to czy w konstruktorze nie trzeba założyć locka? Albo zrobić ewentualnie metodę fabrykującą jako synchronized?

edytowany 2x, ostatnio: Bambo
99xmarcin
A+B: https://stackoverflow.com/a/7596354/1779504 - to zawsze będzie poprawnie działać, bo jest new Holder happen-before join() returns happen-before .execute(...)
AK
  • Rejestracja:ponad 6 lat
  • Ostatnio:około 22 godziny
  • Postów:3561
0

Cząstkowa odp: jak długo trwa wykonanie kodu konstruktora, nikt inny jeszcze nie zna referencji do obiektu, więc w/w kłopoty nie mogą zajść.
Techniki synchronizacyjne w konstruktorze MSZ nie są konieczne. W zwykłych metodach być może tak


Bo C to najlepszy język, każdy uczeń ci to powie
Bambo
No to w w/w książce jest napisane co innego. Autor używa ReetrantLocka w konstruktorze, na stack over flow szukałem czy to było niezbędna i podobno tak, było.
Bambo
Poza tym nawet jak już się konstruktor wykona to może zajść reordering i referencję zobaczysz ale zamiast ustawionego inta zobaczysz 0. JMM.
W0
  • Rejestracja:ponad 12 lat
  • Ostatnio:około 2 godziny
  • Postów:3539
1

Ogólnie:

  1. Java Concurrency in Practice jest dosyć wiekowa. To, co w niej jest jest jak najbardziej prawdziwe, natomiast powstały nowsze zabawki rozwiązujące te problemy.
  2. Twój test jest bardzo dziwny bo tak naprawdę nie wiadomo, co testuje.

Masz dwa wątki (executora pomijam bo to nie ma nic do rzeczy):

  • #1 - wątek główny
  • #2 - wątek obsługujący Thread

Teraz: konstruktor Thread(Runnable) nie wykonuje przekazanego Runnable od razu - tylko po utworzeniu wątku. W rezultacie wątek główny ma szansę, że "dobiegnie" do linijki z assertSanity zanim wykona się Runnable przekazane do Thread.

W przypadku A+B masz Thread.join() - czyli wątek #1 poczeka na #2. W rezultacie Runnable zawsze wykona się przed wykonaniem linijki z asercją.
W przypadku C pole holder zostanie zainicjalizowane jeszcze przed asercją.

Jeśli chodzi o pytanie "bonus" - to zależy co chcesz osiągnąć.

  1. Jeśli obiekt jest niemutowalny to musisz jedynie zapewnić, że będzie zainicjalizowany przed czytaniem.
  2. Jeśli obiekt jest mutowalny, ale nie zależy ci na synchronizacji - to jak wyżej.
  3. Jeśli obiekt jest mutowalny i zależy ci na synchronizacji - to możesz użyć synchronize, użyć zmiennej atomowej (AtomicReference, AtomicInteger itp.), użyć locka, użyć volatile i pewnie jeszcze kilka innych opcji by się znalazło.
edytowany 2x, ostatnio: wartek01
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@wartek01:
Ale odczyt holdera czyli wywołanie metody test assertSanity dzieje się w innych wątkach bez żadnej synchronizacji i z tego co wiem jest to niebezpieczne nawet jak wcześniej już konstruktor się zakończył.

edytowany 1x, ostatnio: Bambo
W0
W twoim kodzie nie ma metody test. Poza tym ja wyjaśniam co tutaj się dzieje, a nie czy to jest bezpieczne czy nie.
Bambo
sorki, assertSanity
W0
No to w tym przypadku NPE leci z powodów wspomnianych wyżej. Jeśli wyrzucisz ExecutorService to wyniki będą podobne. Natomiast jeśli przerzucisz stworzenie ExecutorService zaraz przed for'a to najprawdopodobniej NPE nie będzie leciał - chociaż stuprocentowej pewności nie ma.
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@wartek01: Ale właśnie chodzi o to, że executor odpala to w innych wątkach niż utworzenie Holdera. I NPE leci tylko w przypadku A a na moje w przypadku C oraz A+B też powinien.

Przecież widoczność między wątkami nie jest taka, że zawsze widzi najnowsze wartości.

edytowany 2x, ostatnio: Bambo
W0
  • Rejestracja:ponad 12 lat
  • Ostatnio:około 2 godziny
  • Postów:3539
0

@Bambo: nie, nie chodzi. Kolejne instrukcje kodu są odpalane w głównym wątku, obecność ExecutorService nie ma tutaj nic do rzeczy.

To, że w przypadku A dostajesz NPE wynika z tego, że główny wątek dobiega do asercji zanim ten drugi wątek się wykona. W przypadku A+B i C to nie występuje bo albo to synchronizujesz, albo inicjalizujesz holder'a w głównym wątku.
Dorzuć Thread.sleep(1000) przed forem i też NPE zniknie.

Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@wartek01:
Przeanalizowałem to ze znajomym co robi w watkowosci i wg niego wszystkie 3 przypadki są unsafe xd

edytowany 1x, ostatnio: Bambo
W0
Znajomy się myli, albo ma na myśli inny przypadek niż ten przedstawiony przez ciebie.
pedegie
  • Rejestracja:około 11 lat
  • Ostatnio:ponad rok
  • Postów:204
0

A - to już napisałeś
B - robisz .join() - join to synchronization point, JLS zapewnia, że wszystko co przed, będzie widoczne jak join() zwróci sterowanie do programu
C - w executorze startujesz wątki, start() to również synchronization point, więc to samo co wyżej

https://docs.oracle.com/javase/specs/jls/se11/html/jls-17.html#jls-17.4.5

It follows from the above definitions that:

An unlock on a monitor happens-before every subsequent lock on that monitor.

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

A call to start() on a thread happens-before any actions in the started thread.

All actions in a thread happen-before any other thread successfully returns from a join() on that thread.

The default initialization of any object happens-before any other actions (other than default-writes) of a program.

Jeśli dobrze zrozumiałem pytania, to 2/3 przypadki są w tym konkretnym kodzie SAFE.
Co do pytania bonusowego, to nie, nie trzeba dlatego że start() zapewnia widoczność wszystkiego co było przed. Gdybyś wcześniej stworzył ten wątek a później chciał tylko odczytać dane, czyli pominał synchronization point, to byłoby unsafe

edytowany 1x, ostatnio: pedegie
Bambo
  • Rejestracja:ponad 10 lat
  • Ostatnio:6 miesięcy
  • Postów:779
0

@pedegie:
Dzięki! W książce pominąłem, że start() na Thread oraz join() jest pkt synchronizacji. To jest to happens-before tak?

Mówisz, że konstruktor nie wymaga synchronizacji a jak się odniesiesz do tego wątku https://stackoverflow.com/questions/10528572/java-concurrency-in-practice-sample-14-12 ??

Gdybyś wcześniej stworzył ten wątek a później chciał tylko odczytać dane, czyli pominał synchronization point, to byłoby unsafe

Tutaj nie bardzo rozumiem jak to miało by wyglądać. Mógłbyś na kodzie to pokazać jeśli masz czas oczywiście?

Generalnie prosiłbym też o przykład unsafe publication tak żeby to asset sanity się wykrzaczyło.

pedegie
  • Rejestracja:około 11 lat
  • Ostatnio:ponad rok
  • Postów:204
0
Bambo napisał(a):

Dzięki! W książce pominąłem, że start() na Thread oraz join() jest pkt synchronizacji. To jest to happens-before tak?

happens-before to określenie relacji kolejności wykonywanego kodu - tutaj masz całkiem dobre dwie prezentacje w tej tematyce:

https://www.youtube.com/watch?v=TK-7GCCDF_I
https://www.youtube.com/watch?v=TxqsKzxyySo

Mówisz, że konstruktor nie wymaga synchronizacji a jak się odniesiesz do tego wątku https://stackoverflow.com/questions/10528572/java-concurrency-in-practice-sample-14-12 ??

Ta klasa (i ten przykład z książki) nie odnosi się do żadnego szerszego kontekstu, autor pokazuję jak stworzyć thread-safe Semaphore. Zauważ, że jak tworzymy jakąś klaskę na użytek ogólny (jakieś API po prostu) to nie mamy pojęcia w jaki sposób użytkownicy będą z naszej biblioteki /klasy korzystać. Gdybyśmy mieli gwarancję, że użytkownik tej klasy zawsze najpierw stworzy jej instancję -> następnie będzie synchronization point -> a dopiero póżniej inne wątki będą sie do tego odwołoywać, to ten lock w konstruktorze nie byłby potrzebny. Innymi słowy nie wiesz czy ktoś użyje tej klasy tak:

Kopiuj
semaphore = new Semaphore(5)    // semaphore jest polem klasy, nie lokalnym metody ofc  
Thread t = new Thread(() -> sleep(10); semaphore.cosTam());
t.start();

Czy może jednak tak:

Kopiuj
Thread t = new Thread(() -> sleep(10); semaphore.cosTam());
t.start();
semaphore = new Semaphore(5)     // X

W drugim przypadku, nasz synchronization point był jeszcze przed inicjalizacją semaphore - więc nie mamy w stosunku do niego relacji happens-before, patrząc z perspektywy wątku w którym jest instrukcja "X".
I teraz pomimo tego, że spokojnie zdąży się wykonać instrukcja oznaczona "X" zanim t się obudzi - to możemy właśnie dostać jakiegoś null pointer'a lub zastać pole permits z domyślną wartością 0.

Podsumowując: skoro nie wiem, w jaki sposób będzie moja klasa używana - to powinienem ją napisać w taki sposób, żeby działała zawsze. Więc w tym przypadku pole semaphore mogłoby być oznaczone jako volatile, żeby uniknąć NPE a dodatkowo lock w konstruktorze semaphore'a, żeby mieć pewność że tam na pewno będzie 5 a nie 0. W ten sposób obsłużyliśmy 100% przypadków związanych z synchronizacją tego obiektu.

Gdybyś wcześniej stworzył ten wątek a później chciał tylko odczytać dane, czyli pominał synchronization point, to byłoby unsafe

Tutaj nie bardzo rozumiem jak to miało by wyglądać. Mógłbyś na kodzie to pokazać jeśli masz czas oczywiście?

W zasadzie zreprodukowałeś to już dostając NPE. Chodziło mi o to, że najpierw startujemy wątek -> następnie przypisujemy coś tam do referencji holder a dopiero później z tego** już uruchomionego** wątku próbujemy coś odczytac z holder

Generalnie prosiłbym też o przykład unsafe publication tak żeby to asset sanity się wykrzaczyło.

To prawie graniczy z cudem, żeby ten konkretny przykład z holderem się wywalilł (próbowałem :P), dlatego że:

https://stackoverflow.com/a/40182163/10528516

Of course, according to Murphy’s Law, it will never happen when you try to provoke that error anyway, but once in a while at the customer, but never reproducible…

edytowany 4x, ostatnio: pedegie
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)