Model pamięci Java (JMM) gwarantuje, że po zakończeniu konstruktora zapisy do pól final stają się widoczne dla każdego wątku, który odczytuje odniesienie do obiektu, pod warunkiem, że to odniesienie nie wydostało się podczas konstrukcji. Jeśli odniesienie this wycieka przedwcześnie — poprzez przekazanie do innego wątku lub zapisanie w kolekcji statycznej przed zwróceniem konstruktora — krawędź happens-before pomiędzy zapisem konstruktora do pola final a odczytem w innym wątku jest przerywana. W konsekwencji, wątek obserwujący może zaobserwować wartość domyślną (zero, false lub null) zamiast skonstruowanej wartości, łamiąc pozorną niezmienność. Bezpieczne publikowanie wymaga, aby żadne odniesienie do obiektu w trakcie konstrukcji nie wydostawało się aż do zakończenia konstrukcji, co zapewnia, że akcja zamrożenia pól final następuje przed tym, jak jakikolwiek wątek załadować odniesienie.
Spotkaliśmy się z tym w systemie handlu wysokiej częstotliwości, gdzie instancje Service rejestrowały się w globalnym ConcurrentHashMap podczas swoich konstruktorów, aby ułatwić wyszukiwanie. Klasa definiowała final long instrumentId, inicjowaną z parametru konstruktora, jednak wątki monitorujące sporadycznie odczytywały zero, gdy sprawdzały rejestr natychmiast po utworzeniu.
Jednym z proponowanych rozwiązań było zadeklarowanie instrumentId jako volatile zamiast final, mając nadzieję na wymuszenie natychmiastowej widoczności między rdzeniami. To podejście gwarantowało atomowość i widoczność, ale rezygnowało z umowy o niezmienności i wiązało się z pełnym kosztem bariery pamięci przy każdym odczycie, niepotrzebnie obniżając przepustowość dla wartości, która nigdy się nie zmieniała po konstrukcji i komplikując rozumienie stanu obiektu.
Inną sugestią było synchronizowanie wszystkich dostępu do rejestru za pomocą bloków synchronized otaczających logikę konstruktora, teoretyzując, że blokada wyczyści pamięci podręczne. Chociaż to zapobiegało warunkom wyścigu, wprowadzało dużą kontencję na globalnej blokadzie rejestru, przekształcając strukturę współbieżną w seryjny wąskie gardło i łamiąc rygorystyczne wymogi dotyczące opóźnienia dla przetwarzania danych rynkowych.
Wybraliśmy wzorzec fabryki, który oddzielił instancjonowanie od rejestracji. Konstruktory pozostały prywatne, metoda fabryki wywoływała new Service(id) w całości, a dopiero później publikowała w pełni uformowane odniesienie do ConcurrentHashMap. Dzięki temu wykorzystano semantykę zamrożenia pól final JMM bez narzutu synchronizacji, zapewniając, że instrumentId był widoczny natychmiast po pobraniu.
Zmiana ta usunęła anomalie widoczności wartości zerowej i przywróciła oczekiwane opóźnienie rzędu mikrosekund dla wyszukiwania usług, przy jednoczesnym zachowaniu zamiaru projektowania jako niezmiennego.
Dlaczego final nie gwarantuje widoczności, jeśli po prostu publikuję odniesienie przez wątkowo bezpieczną kolekcję, jak ConcurrentHashMap?
Relacja happens-before zapewniana przez operacje put i get ConcurrentHashMap ustala porządek pomiędzy wewnętrznymi zmianami stanu mapy, a nie pomiędzy zapisami konstruktora a publikacją mapy. Jeśli odniesienie this wydostaje się podczas konstrukcji, zapis do pola final następuje w jednym wątku, podczas gdy publikacja mapy odbywa się równolegle, brakując krawędzi happens-before potrzebnej do zapobieżenia przegrupowaniu instrukcji. Dlatego wątek odczytujący może zaobserwować odniesienie przez mapę przed zapisami konstruktora, które są wyczyszczone do pamięci głównej, obserwując wartość domyślną.
Czy mogę to naprawić, czyniąc pole rejestru volatile zamiast pól obiektu?
Oznaczenie odniesienia do rejestru jako volatile zapewnia tylko, że zmiany zmiennej rejestru są widoczne, a nie wewnętrzny stan obiektów, które zawiera. Ponieważ problem polega na czasie zapisów pól obiektu w stosunku do odniesienia, które staje się widoczne, volatile dla kontenera nie ustanawia koniecznego porządku pomiędzy konstruktorem a konsumentem obiektu. Nadal można by zaobserwować częściowo skonstruowane instancje.
Czy używanie synchronized wewnątrz konstruktora zapobiega niebezpiecznej publikacji?
Umieszczenie synchronized na konstruktorze lub użycie go do ochrony rejestracji zapobiega innym wątkom na równoległe wchodzenie do krytycznej sekcji, ale nie zapobiega wydostawaniu się odniesienia this, jeśli metoda rejestracji wycieka odniesienie do wątku, który działa poza tą blokadą. JMM wymaga, aby żadne odniesienie do obiektu nie wydostało się przed zakończeniem konstruktora, aby semantyka pól final mogła obowiązywać; synchronizacja bez odpowiedniego porządku publikacji nie może przywrócić tej gwarancji.