Bariery pamięci i styl kodowania na maszynie wirtualnej Java

Załóżmy, że mam statyczny obiekt złożony, który jest okresowo aktualizowany przez pulę wątków i czytany mniej więcej w sposób ciągły w długotrwałym wątku. Sam obiekt jest zawsze niezmienny i odzwierciedla najnowszy stan czegoś.

class Foo() { int a, b; }
static Foo theFoo;
void updateFoo(int newA, int newB) {
  f = new Foo();
  f.a = newA;
  f.b = newB;
  // HERE
  theFoo = f;
}
void readFoo() {
  Foo f = theFoo;
  // use f...
}

Nie obchodzi mnie, czy mój czytelnik widzi stary czy nowy Foo, jednak muszę zobaczyć w pełni zainicjowany obiekt. IUC, Specyfikacja Javy mówi, że bez bariery pamięci tutaj, mogę zobaczyć obiekt z F. B zainicjowany, ale f. a jeszcze nie zapamiętywane. Mój program jest prawdziwym programem, który prędzej czy później przeczyta rzeczy do pamięci, więc nie muszę od razu przeczytywać nowej wartości theFoo do pamięci(choć nie zaszkodzi).

Jaki jest według ciebie najbardziej czytelny sposób implementacji bariery pamięci ? Jestem gotów zapłacić trochę ceny wydajności ze względu na czytelność, jeśli zajdzie taka potrzeba. Myślę, że mogę zsynchronizować zadanie z Foo i to by zadziałało, ale nie jestem pewien, czy to bardzo oczywiste dla kogoś czytającego kod, dlaczego to robię. Mógłbym również zsynchronizować całą inicjalizację nowego Foo, ale to wprowadziłoby więcej blokowania, które faktycznie potrzebne.

Jak byś to napisał, żeby było jak najbardziej czytelne ?
Bonus za wersję Scali:)

Author: Jean, 2010-10-19

2 answers

Krótkie odpowiedzi na oryginalne pytanie

  • Jeśli Foo jest niezmienne, po prostu ukończenie pól zapewni pełną inicjalizację i spójną widoczność pól dla wszystkich wątków, niezależnie od synchronizacji.
  • niezależnie od tego, czy Foo jest niezmienna, publikacja przez volatile theFoo lub AtomicReference<Foo> theFoo jest wystarczająca, aby zapewnić, że zapisy do jej pól są widoczne dla dowolnego wątku czytającego przez theFoo reference
  • używając zwykłego przypisania do theFoo, wątki czytnika są nigdy gwarantujemy, że zobaczysz jakąkolwiek aktualizację
  • W 1995 roku, w wyniku połączenia z JCiP, powstała pierwsza w Polsce Wersja jcip, która została wprowadzona do użytku w 1996 roku, a w 1998 roku została wprowadzona do użytku w 1999 roku.]} Niestety nie mam nic do zaoferowania w Scali]}

Możesz użyć volatile

Winię cię. Teraz jestem uzależniony, złamałem JCiP , a teraz zastanawiam się, czy jakikolwiek kod, który kiedykolwiek napisałem, jest poprawny. Powyższy fragment kodu jest potencjalnie niespójna. (Edit: zobacz sekcję poniżej na temat bezpiecznej publikacji za pośrednictwem volatile.) wątek odczytu może również widzieć stale (w tym przypadku, bez względu na domyślne wartości dla a i b były) dla nieograniczonego czasu.Możesz wykonać jedną z następujących czynności, aby wprowadzić krawędź happens-before:]}
    W ten sposób można uzyskać dostęp do informacji o tym, co dzieje się w danym miejscu, a co za tym idzie, o tym, co dzieje się w danym miejscu.]}
  • użyj final pól i inicjalizacja wartości w konstruktorze przed publikacją
  • wprowadzenie zsynchronizowanego bloku podczas zapisu nowych wartości do theFoo obiektu
  • użyj AtomicInteger pól

To rozwiązuje kolejność zapisu (i rozwiązuje problemy z widocznością). Następnie musisz zająć się widocznością nowego odniesienia theFoo. Tutaj volatile jest właściwe -- jcip mówi w sekcji 3.1.4 "zmienne lotne", (a tutaj zmienna jest theFoo):

Możesz użyć zmienne zmienne tylko wtedy, gdy spełnione są wszystkie następujące kryteria:
  • zapis do zmiennej nie zależy od jej bieżącej wartości, lub możesz upewnić się, że tylko jeden wątek kiedykolwiek aktualizuje wartość;
  • zmienna nie uczestniczy w niezmiennikach z innymi zmiennymi stanu; i
  • Blokowanie nie jest wymagane z żadnego innego powodu, gdy zmienna jest dostępna

Jeśli wykonasz następujące czynności, jesteś złoty:

class Foo { 
  // it turns out these fields may not be final, with the volatile publish, 
  // the values will be seen under the new JMM
  final int a, b; 
  Foo(final int a; final int b) 
  { this.a = a; this.b=b; }
}

// without volatile here, separate threads A' calling readFoo()
// may never see the new theFoo value, written by thread A 
static volatile Foo theFoo;
void updateFoo(int newA, int newB) {
  f = new Foo(newA,newB);
  theFoo = f;
}
void readFoo() {
  final Foo f = theFoo;
  // use f...
}

Proste i czytelne

Kilku ludzi w tym i innych wątkach (dzięki @John V) zauważ, że władze w tych kwestiach podkreślają znaczeniedokumentacji zachowania synchronizacji i założeń. JCiP mówi o tym szczegółowo, dostarcza zestaw adnotacji , które mogą być używane do dokumentacji i sprawdzania statycznego, a także można zajrzeć do JMM Cookbook dla wskaźników dotyczących konkretnych zachowań, które wymagają dokumentacji oraz odnośniki do odpowiednich odnośników. Doug Lea przygotował również listę problemów do rozważenia, kiedy {111]}dokumentuje zachowanie współbieżności {66]}. Dokumentacja jest odpowiednia szczególnie ze względu na obawy, sceptycyzm i zamieszanie związane z kwestiami współbieżności(on SO: {113]} " czy cynizm współbieżności Javy posunął się za daleko?"). Ponadto narzędzia takie jak FindBugs zapewniają teraz statyczne reguły sprawdzania, aby zauważać naruszenia semantyki adnotacji JCiP, takie jak "niespójna Synchronizacja: IS_FIELD-NOT_GUARDED".

Dopóki uważasz, że masz powód, aby zrobić inaczej, prawdopodobnie najlepiej jest przejść do najbardziej czytelnego rozwiązania, coś takiego (dzięki, @ Burleigh Bear), używając @Immutable oraz @GuardedBy Przypisy

@Immutable
class Foo { 
  final int a, b; 
  Foo(final int a; final int b) { this.a = a; this.b=b; }
}

static final Object FooSync theFooSync = new Object();

@GuardedBy("theFooSync");
static Foo theFoo;

void updateFoo(final int newA, final int newB) {
  f = new Foo(newA,newB);
  synchronized (theFooSync) {theFoo = f;}
}
void readFoo() {
  final Foo f;
  synchronized(theFooSync){f = theFoo;}
  // use f...
}

Lub, być może, ponieważ jest czystsze:

static AtomicReference<Foo> theFoo;

void updateFoo(final int newA, final int newB) {
  theFoo.set(new Foo(newA,newB)); }
void readFoo() { Foo f = theFoo.get(); ... }

Kiedy należy stosować volatile

Po pierwsze, zauważ, że to pytanie odnosi się do pytania tutaj, ale był adresowany wiele, wiele razy na tak:

W rzeczywistości, wyszukiwarka google: "site:stackoverflow.com + java + volatile +keyword" zwraca 355 różnych wyników. Użycie volatile to w najlepszym razie niestabilna decyzja. Kiedy to właściwe? JCiP podaje pewne abstrakcyjne wytyczne (cytowane powyżej). Zbieram kilka praktycznych wskazówek tutaj:

  • podoba mi się ta odpowiedź: "volatile can be used to safely publish immutable objects", który starannie hermetyzuje większość zakresu zastosowań, jakich można oczekiwać od programisty aplikacji.
  • @mdma ' s odpowiedz tutaj: "volatile jest najbardziej przydatna w algorytmach bez blokady " podsumowuje kolejną klasę uses - algorytmy specjalnego przeznaczenia, bez blokady, które są wystarczająco wrażliwe na wydajność, aby zasługiwać na dokładną analizę i walidację przez eksperta.

  • Bezpieczna publikacja przez lotne

    Idąc dalej @ Jed Wesley-Smith, wydaje się, że volatile zapewnia teraz silniejsze Gwarancje (od JSR-133), A wcześniejsze stwierdzenie "możesz użyć volatile pod warunkiem, że opublikowany obiekt jest niezmienny" jest wystarczające, ale być może nie jest konieczne.

    Patrząc na FAQ JMM, dwa wpisy jak działają Ostatnie pola pod nowym JMM? i co robi volatile?Nie są tak naprawdę rozpatrywane razem, ale myślę, że druga daje nam to, czego potrzebujemy:]}

    Różnica polega na tym, że teraz nie już tak łatwo zmienić kolejność normalnego pola dostęp do nich. Pisanie do a pole lotne ma taką samą pamięć efekt jako zwolnienie monitora, oraz odczyt z pola lotnego ma taki sam efekt pamięci Jak monitor / align = "left" / W efekcie, ponieważ nowy miejsca modelu pamięci ograniczenia dotyczące zmiany kolejności lotnych dostęp do pola z innym polem dostęp, Lotny czy nie, cokolwiek to było widoczne dla wątku a kiedy to zapisuje do pola lotnego f widoczne dla wątku B, gdy czyta f.

    Zauważ, że pomimo kilku przekierowań JCiP, odpowiedni tekst nie wyskoczył mi, dopóki Jed nie wskazał go. Jest na str. 38, sekcja 3.1.4, i mówi mniej więcej to samo, co poprzedni cytat - opublikowany obiekt musi być skutecznie niezmienny, bez final pól wymaganych, QED.

    Starsze rzeczy, trzymane za odpowiedzialność

    Jeden komentarz: jakiś powód, dla którego newA i newB nie mogą być argumentami dla konstruktora? Wtedy możesz polegać na regułach publikacji dla konstruktorów...

    Również, używając AtomicReference prawdopodobnie usuwa wszelką niepewność (i może kupić ci inne korzyści w zależności od tego, co musisz zrobić w pozostałej części klasy... Ponadto, ktoś mądrzejszy ode mnie może Ci powiedzieć, czy to rozwiąże, ale zawsze wydaje mi się to tajemnicze...

    W dalszej recenzji, uważam, że komentarz od @ Burleigh Bear powyżej jest poprawny - - - (EDIT: patrz poniżej) nie musisz się martwić o kolejność out-of-sequence, ponieważ publikujesz nowy obiekt do theFoo. Podczas gdy inny wątek może prawdopodobnie zobaczyć niespójne wartości dla newA i newB, jak opisano w JLS 17.11, to nie może dzieje się tak, ponieważ zostaną one zapisane w pamięci, zanim drugi wątek uzyska odniesienie do nowej instancji f = new Foo(), którą utworzyłeś... jest to bezpieczna Jednorazowa Publikacja. Z drugiej strony, jeśli napisałeś

    void updateFoo(int newA, int newB) {
      f = new Foo(); theFoo = f;     
      f.a = newA; f.b = newB;
    }
    

    Ale w tym przypadku problemy z synchronizacją są dość przejrzyste, a zamawianie jest najmniejszym z twoich zmartwień. Aby uzyskać przydatne wskazówki na temat volatile, zapoznaj się z tym artykułem developerWorks.

    Jednakże, możesz mieć problem, gdzie osobne wątki czytnika mogą zobaczyć starą wartość theFoo dla nieograniczonej ilości czasu. W praktyce zdarza się to rzadko. Jednak JVM może mieć możliwość buforowania wartości odniesienia theFoo w kontekście innego wątku. Jestem pewien, że Oznaczenie theFoo jako volatile zajmie się tym, podobnie jak każdy rodzaj synchronizatora lub AtomicReference.

     45
    Author: andersoj,
    Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
    2017-05-23 12:32:08

    Posiadanie niezmiennego Foo z końcowymi polami a i b rozwiązuje problemy z widocznością z wartościami domyślnymi, ale również sprawia, że foo jest niestabilne.

    Osobiście lubię mieć niezmienne klasy wartości i tak, ponieważ znacznie trudniej je nadużywać.

     4
    Author: Jed Wesley-Smith,
    Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
    2010-10-19 00:17:03