Do czego służą ogrodzenia pamięci w Javie?

18

Próbując zrozumieć, w jaki sposób SubmissionPublisher( kod źródłowy w Java SE 10, OpenJDK | docs ), nowa klasa dodana do Java SE w wersji 9, została zaimplementowana, natknąłem się na kilka wywołań API, VarHandlektórych wcześniej nie byłem świadomy:

fullFence, acquireFence, releaseFence, loadLoadFenceI storeStoreFence.

Po przeprowadzeniu pewnych badań, szczególnie w odniesieniu do koncepcji barier / płotów pamięci (słyszałem o nich wcześniej, tak; ale nigdy ich nie użyłem, a zatem nie byłem zaznajomiony z ich semantyką), myślę, że mam podstawową wiedzę na temat tego, do czego służą . Niemniej jednak, ponieważ moje pytania mogą wynikać z nieporozumień, przede wszystkim chcę się upewnić, że dobrze to zrozumiałem:

  1. Bariery pamięci zmieniają ograniczenia dotyczące operacji odczytu i zapisu.

  2. Bariery pamięci można podzielić na dwie główne kategorie: jednokierunkowe i dwukierunkowe bariery pamięci, w zależności od tego, czy nakładają ograniczenia na odczyty, zapisy czy oba.

  3. C ++ obsługuje różne bariery pamięci , jednak nie pasują one do tych dostarczonych przez VarHandle. Jednak niektóre z dostępnych barier pamięci VarHandlezapewniają efekty porządkowania, które są zgodne z odpowiadającymi im barierami pamięci C ++.

    • #fullFence jest kompatybilny z atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence jest kompatybilny z atomic_thread_fence(memory_order_acquire)
    • #releaseFence jest kompatybilny z atomic_thread_fence(memory_order_release)
    • #loadLoadFencei #storeStoreFencenie mają kompatybilnej części licznika C ++

Słowo „ kompatybilny” wydaje się tutaj naprawdę ważne, ponieważ semantyka wyraźnie różni się, jeśli chodzi o szczegóły. Na przykład wszystkie bariery C ++ są dwukierunkowe, podczas gdy bariery Java nie są (koniecznie).

  1. Większość barier pamięci ma również efekty synchronizacji. Te w szczególności zależą od zastosowanego rodzaju bariery i wcześniej wykonanych instrukcji barier w innych wątkach. Ponieważ pełne implikacje instrukcji barierowej są specyficzne dla sprzętu, pozostanę przy barierach wyższego poziomu (C ++). Na przykład w C ++ zmiany dokonane przed instrukcją bariery zwolnienia są widoczne dla wątku wykonującego instrukcję bariery nabywania .

Czy moje założenia są prawidłowe? Jeśli tak, moje pytania wynikowe to:

  1. Czy dostępne bariery pamięci VarHandlepowodują jakąkolwiek synchronizację pamięci?

  2. Niezależnie od tego, czy powodują synchronizację pamięci, czy nie, jakie zmiany w ograniczaniu kolejności mogą być przydatne w Javie? Model pamięci Java daje już pewne bardzo mocne gwarancje dotyczące zamawiania przy zmiennych polach, blokadach lub VarHandlepodobnych operacjach #compareAndSet.

Jeśli szukasz przykładu: wyżej wspomniana BufferedSubscriptionklasa wewnętrzna SubmissionPublisher(źródło połączone powyżej) ustanowiła pełne ogrodzenie w linii 1079 (funkcja growAndAdd; ponieważ połączona witryna nie obsługuje identyfikatorów fragmentów, wystarczy CTRL + F dla tego ). Nie jest jednak dla mnie jasne, do czego służy.

Kafel
źródło
1
Próbowałem odpowiedzieć, ale mówiąc bardzo prosto, istnieją, ponieważ ludzie chcą trybu słabszego niż Java. W kolejności rosnącej, to będzie: plain -> opaque -> release/acquire -> volatile (sequential consistency).
Eugene

Odpowiedzi:

11

Jest to głównie brak odpowiedzi, naprawdę (początkowo chciałem, aby był to komentarz, ale jak widać, jest o wiele za długi). Tyle, że sam to często przesłuchiwałem, dużo czytałem i prowadziłem badania, a teraz mogę spokojnie powiedzieć: to skomplikowane. Napisałem nawet wiele testów za pomocą jcstress, aby dowiedzieć się, jak naprawdę działają (patrząc na wygenerowany kod asemblera) i chociaż niektóre z nich mają jakikolwiek sens, temat w ogóle nie jest łatwy.

Pierwszą rzeczą, którą musisz zrozumieć:

Specyfikacja języka Java (JLS) nigdzie nie wspomina o barierach . Dla Javy byłby to szczegół implementacji: naprawdę działa, jeśli chodzi o dzieje przed semantyką. Aby móc poprawnie określić je zgodnie z JMM (Java Memory Model), JMM musiałby się sporo zmienić .

To jest praca w toku.

Po drugie, jeśli naprawdę chcesz zarysować powierzchnię, jest to pierwsza rzecz do obejrzenia . Rozmowa jest niesamowita. Moją ulubioną częścią jest to, że Herb Sutter podnosi 5 palców i mówi: „Tyle osób może naprawdę i poprawnie z nimi pracować”. To powinno dać ci wskazówkę co do złożoności. Niemniej jednak istnieją pewne trywialne przykłady, które są łatwe do uchwycenia (np. Licznik aktualizowany przez wiele wątków, który nie dba o inne gwarancje pamięci, ale tylko dba o to, aby sam został poprawnie zwiększony).

Innym przykładem jest sytuacja, gdy (w java) chcesz, aby volatileflaga kontrolowała wątki w celu zatrzymania / uruchomienia. Wiesz, klasyczny:

volatile boolean stop = false; // on thread writes, one thread reads this    

Jeśli pracujesz z Javą, będziesz wiedział, że bez volatile tego kodu jest zepsuty (możesz na przykład przeczytać, dlaczego blokowanie podwójnego sprawdzania jest bez niego przerwane). Ale czy wiesz również, że dla niektórych osób, które piszą kod o wysokiej wydajności, to za dużo? volatileodczyt / zapis gwarantuje także sekwencyjną spójność - ma pewne mocne gwarancje, a niektórzy chcą słabszej wersji tego.

Flaga bezpieczna dla wątku, ale niestabilna? Tak, dokładnie: VarHandle::set/getOpaque.

Zastanawiasz się, dlaczego ktoś może tego potrzebować? Nie wszyscy są zainteresowani wszystkimi zmianami, które są wspierane przez volatile.

Zobaczmy, jak to osiągniemy w java. Przede wszystkim takie egzotyczne rzeczy istniały już w API: AtomicInteger::lazySet. Nie jest to określone w modelu pamięci Java i nie ma jasnej definicji ; wciąż ludzie go używali (LMAX, afaik lub ten, aby przeczytać więcej ). IMHO, AtomicInteger::lazySetto VarHandle::releaseFence(lub VarHandle::storeStoreFence).


Spróbujmy odpowiedzieć, dlaczego ktoś tego potrzebuje ?

JMM ma zasadniczo dwa sposoby dostępu do pola: zwykły i zmienny (co gwarantuje sekwencyjną spójność ). Wszystkie te metody, o których wspominasz, służą wprowadzeniu czegoś pomiędzy tymi dwoma - uwolnieniu / zdobyciu semantyki ; zdaje się, że są przypadki, w których ludzie naprawdę tego potrzebują.

Jeszcze bardziej relaksujący od wydania / nabycia byłby nieprzejrzysty , co wciąż próbuję w pełni zrozumieć .


Podsumowując (twoje rozumienie jest dość poprawne, przy okazji): jeśli planujesz użyć tego w java - nie mają one obecnie specyfikacji, zrób to na własne ryzyko. Jeśli chcesz je zrozumieć, zacznij od ich równoważnych trybów C ++.

Eugene
źródło
1
Nie próbuj odgadnąć znaczenia lazySet, łącząc się ze starożytnymi odpowiedziami, obecna dokumentacja precyzyjnie mówi, co to znaczy w dzisiejszych czasach. Ponadto mylące jest twierdzenie, że JMM ma tylko dwa tryby dostępu. Mamy niestabilny odczyt i niestabilny zapis , które razem mogą ustanowić relację przed zdarzeniem.
Holger
1
Byłem w trakcie pisania czegoś więcej na ten temat. Pomyśl, że cas jest zarówno czytaniem, jak i pisaniem, działając jak pełna bariera, i możesz zrozumieć, dlaczego relaks jest pożądany. Np. Przy implementacji blokady, pierwszą czynnością jest cas (0, 1) na podstawie liczby blokad, ale wystarczy tylko przyswoić semantyczny (jak zmienny odczyt), podczas gdy ostateczny zapis 0 dla odblokowania powinien mieć semantyczny uwolnienie (jak zmienny zapis ), więc przed odblokowaniem i późniejszym zablokowaniem dzieje się zdarzenie. Acquire / Release jest jeszcze słabszy niż Volatile Read / Write w odniesieniu do wątków korzystających z różnych blokad.
Holger
1
@Peter Cordes: Pierwsza wersja C ze volatilesłowem kluczowym to C99, pięć lat po Javie, ale wciąż brakowało użytecznej semantyki, nawet C ++ 03 nie ma modelu pamięci. Rzeczy, które C ++ nazywa „atomowymi”, są również znacznie młodsze niż Java. Słowo volatilekluczowe nie oznacza nawet aktualizacji atomowych. Dlaczego więc miałby tak się nazywać.
Holger
1
@PeterCordes, być może, mylę to restrict, jednak pamiętam czasy, kiedy musiałem pisać, __volatileaby użyć rozszerzenia kompilatora niebędącego słowem kluczowym. Więc może nie wdrożył całkowicie C89? Nie mów mi, że jestem taki stary. Przed Javą 5 volatilebył znacznie bliższy C. Jednak Java nie miała MMIO, więc jej celem zawsze było wielowątkowość, ale semantyczna wersja wcześniejsza niż Java 5 nie była do tego bardzo przydatna. Dodano więc wydanie / akwizycję jak semantykę, ale nie jest to atomowe (aktualizacje atomowe to dodatkowa funkcja zbudowana na jej szczycie).
Holger
2
@Eugene w związku z tym , mój przykład był konkretny dla użycia cas do blokowania, które można uzyskać. Zatrzask odliczający nosiłby dekrety atomowe z semantycznym uwalnianiem, po czym nić osiągała zero, wstawiając ogrodzenie nabywania i wykonując ostateczną akcję. Oczywiście istnieją inne przypadki aktualizacji atomowych, w których wymagane jest pełne ogrodzenie.
Holger