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, VarHandle
których wcześniej nie byłem świadomy:
fullFence
, acquireFence
, releaseFence
, loadLoadFence
I 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:
Bariery pamięci zmieniają ograniczenia dotyczące operacji odczytu i zapisu.
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.
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ęciVarHandle
zapewniają efekty porządkowania, które są zgodne z odpowiadającymi im barierami pamięci C ++.#fullFence
jest kompatybilny zatomic_thread_fence(memory_order_seq_cst)
#acquireFence
jest kompatybilny zatomic_thread_fence(memory_order_acquire)
#releaseFence
jest kompatybilny zatomic_thread_fence(memory_order_release)
#loadLoadFence
i#storeStoreFence
nie 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).
- 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:
Czy dostępne bariery pamięci
VarHandle
powodują jakąkolwiek synchronizację pamięci?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
VarHandle
podobnych operacjach#compareAndSet
.
Jeśli szukasz przykładu: wyżej wspomniana BufferedSubscription
klasa 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.
plain -> opaque -> release/acquire -> volatile (sequential consistency)
.Odpowiedzi:
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ć:
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
volatile
flaga kontrolowała wątki w celu zatrzymania / uruchomienia. Wiesz, klasyczny: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?volatile
odczyt / zapis gwarantuje także sekwencyjną spójność - ma pewne mocne gwarancje, a niektórzy chcą słabszej wersji tego.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::lazySet
toVarHandle::releaseFence
(lubVarHandle::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 ++.
źródło
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.volatile
sł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łowovolatile
kluczowe nie oznacza nawet aktualizacji atomowych. Dlaczego więc miałby tak się nazywać.restrict
, jednak pamiętam czasy, kiedy musiałem pisać,__volatile
aby 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ą 5volatile
był 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).