JavaRush /Blog Java /Random-PL /Zarządzanie zmiennością
lexmirnov
Poziom 29
Москва

Zarządzanie zmiennością

Opublikowano w grupie Random-PL

Wytyczne dotyczące stosowania zmiennych zmiennych

Brian Goetz, 19 czerwca 2007 r Oryginał: Zarządzanie zmiennością Zmienne lotne w Javie można nazwać „zsynchronizowanym światłem”; Wymagają mniej kodu do użycia niż bloki zsynchronizowane, często działają szybciej, ale mogą wykonać tylko ułamek tego, co robią bloki zsynchronizowane. W tym artykule przedstawiono kilka wzorców skutecznego używania volatile’u oraz kilka ostrzeżeń o tym, gdzie nie należy go używać. Blokady mają dwie główne cechy: wzajemne wykluczanie (mutex) i widoczność. Wzajemne wykluczanie oznacza, że ​​blokadę może utrzymać tylko jeden wątek na raz, a tej właściwości można użyć do zaimplementowania protokołów kontroli dostępu do współdzielonych zasobów, tak aby w danym momencie korzystał z nich tylko jeden wątek. Widoczność to kwestia bardziej subtelna, jej celem jest zapewnienie, że zmiany wprowadzone w zasobach publicznych przed zwolnieniem blokady będą widoczne dla następnego wątku, który przejmie tę blokadę. Gdyby synchronizacja nie gwarantowała widoczności, wątki mogłyby otrzymywać nieaktualne lub nieprawidłowe wartości zmiennych publicznych, co prowadziłoby do szeregu poważnych problemów.
Zmienne zmienne
Zmienne lotne mają właściwości widoczności zsynchronizowanych, ale brakuje im ich atomowości. Oznacza to, że wątki będą automatycznie korzystać z najbardziej aktualnych wartości zmiennych zmiennych. Można je stosować dla bezpieczeństwa wątków , ale w bardzo ograniczonym zestawie przypadków: takich, które nie wprowadzają relacji pomiędzy wieloma zmiennymi lub pomiędzy bieżącymi i przyszłymi wartościami zmiennej. Zatem samo volatile nie wystarczy do zaimplementowania licznika, muteksu lub dowolnej klasy, której niezmienne części są powiązane z wieloma zmiennymi (na przykład „start <= koniec”). Możesz wybrać blokady lotne z jednego z dwóch głównych powodów: prostoty lub skalowalności. Niektóre konstrukcje językowe są łatwiejsze do napisania jako kod programu, a później do odczytania i zrozumienia, gdy zamiast blokad używają zmiennych zmiennych. Ponadto, w przeciwieństwie do blokad, nie mogą blokować wątku i dlatego są mniej podatne na problemy ze skalowalnością. W sytuacjach, gdy jest znacznie więcej odczytów niż zapisów, zmienne zmienne mogą zapewnić lepszą wydajność w porównaniu z blokadami.
Warunki prawidłowego stosowania substancji lotnych
W ograniczonej liczbie przypadków możesz wymienić zamki na niestabilne. Aby zapewnić bezpieczeństwo wątków, muszą zostać spełnione oba kryteria:
  1. To, co jest zapisywane w zmiennej, jest niezależne od jej bieżącej wartości.
  2. Zmienna nie uczestniczy w niezmiennikach z innymi zmiennymi.
Mówiąc najprościej, warunki te oznaczają, że prawidłowe wartości, które można zapisać w zmiennej niestabilnej, są niezależne od jakiegokolwiek innego stanu programu, w tym od bieżącego stanu zmiennej. Pierwszy warunek wyklucza użycie zmiennych zmiennych jako liczników bezpiecznych dla wątków. Chociaż inkrementacja (x++) wygląda jak pojedyncza operacja, w rzeczywistości jest to cała sekwencja operacji odczytu, modyfikacji i zapisu, które należy wykonać niepodzielnie, czego nie zapewnia volatile. Prawidłowa operacja wymagałaby, aby wartość x pozostała taka sama przez całą operację, czego nie można osiągnąć przy użyciu volatile. (Jeśli jednak możesz mieć pewność, że wartość jest zapisywana tylko z jednego wątku, pierwszy warunek można pominąć.) W większości sytuacji naruszony zostanie pierwszy lub drugi warunek, co sprawi, że zmienne zmienne będą rzadziej stosowanym podejściem do zapewnienia bezpieczeństwa wątku niż zsynchronizowane. Listing 1 przedstawia klasę, która nie jest bezpieczna dla wątków, z zakresem liczb. Zawiera niezmiennik - dolna granica jest zawsze mniejsza lub równa górnej. @NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } } Ponieważ zmienne stanu zakresu są w ten sposób ograniczone, nie wystarczy, aby dolne i górne pola były zmienne, aby zapewnić bezpieczeństwo wątków; synchronizacja będzie nadal potrzebna. W przeciwnym razie prędzej czy później będziesz miał pecha i dwa wątki wykonujące setLower() i setUpper() z niewłaściwymi wartościami mogą doprowadzić zakres do niespójnego stanu. Na przykład, jeśli wartość początkowa to (0, 5), wątek A wywołuje setLower(4), a jednocześnie wątek B wywołuje setUpper(3), te przeplatane operacje spowodują błąd, chociaż obie przejdą kontrolę to ma chronić niezmiennik. W rezultacie zakres będzie wynosił (4, 3) - nieprawidłowe wartości. Musimy sprawić, że setLower() i setUpper() będą atomowe w stosunku do innych operacji na zakresach - a uczynienie pól niestabilnymi tego nie zrobi.
Rozważania dotyczące wydajności
Pierwszym powodem użycia volatile jest prostota. W niektórych sytuacjach użycie takiej zmiennej jest po prostu łatwiejsze niż użycie powiązanej z nią blokady. Drugim powodem jest wydajność, czasami lotne będą działać szybciej niż blokady. Niezwykle trudno jest sformułować precyzyjne, wszechstronne stwierdzenia, takie jak „X jest zawsze szybsze niż Y”, zwłaszcza jeśli chodzi o wewnętrzne operacje wirtualnej maszyny Java. (Na przykład maszyna JVM może w niektórych sytuacjach całkowicie zwolnić blokadę, co utrudnia abstrakcyjne omawianie kosztów zmiennych i synchronizacji). Jednakże w większości nowoczesnych architektur procesorów koszt odczytu zmiennych nie różni się zbytnio od kosztu odczytu zmiennych zwykłych. Koszt pisania zmiennych jest znacznie wyższy niż pisanie zwykłych zmiennych ze względu na ogrodzenie pamięci wymagane do zapewnienia widoczności, ale ogólnie jest tańsze niż ustawianie blokad.
Wzorce prawidłowego wykorzystania lotności
Wielu ekspertów ds. współbieżności ma tendencję do całkowitego unikania zmiennych zmiennych, ponieważ są one trudniejsze w prawidłowym użyciu niż blokady. Istnieją jednak pewne dobrze zdefiniowane wzorce, które, jeśli będą dokładnie przestrzegane, można bezpiecznie stosować w różnorodnych sytuacjach. Zawsze przestrzegaj ograniczeń volatile - używaj tylko zmiennych, które są niezależne od czegokolwiek innego w programie, a to powinno uchronić cię przed wejściem na niebezpieczne terytorium z tymi wzorcami.
Wzór nr 1: Flagi stanu
Być może kanonicznym zastosowaniem zmiennych zmiennych są proste flagi statusu logicznego wskazujące, że wystąpiło ważne jednorazowe zdarzenie w cyklu życia, takie jak zakończenie inicjalizacji lub żądanie zamknięcia. Wiele aplikacji zawiera konstrukcję kontrolną w postaci: „dopóki nie będziemy gotowi do zamknięcia, kontynuuj działanie”, jak pokazano na Listingu 2: Jest prawdopodobne, volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } że metoda zamknięcia() zostanie wywołana skądś spoza pętli – w innym wątku – dlatego wymagana jest synchronizacja, aby zapewnić poprawną widoczność zmiennej. ShutdownRequested. (Można go wywołać ze słuchacza JMX, słuchacza akcji w wątku zdarzeń GUI, za pośrednictwem RMI, usługi internetowej itp.). Jednakże pętla ze zsynchronizowanymi blokami będzie znacznie bardziej uciążliwa niż pętla z flagą stanu volatile jak na Listingu 2. Ponieważ volatile ułatwia pisanie kodu, a flaga stanu nie zależy od żadnego innego stanu programu, jest to przykład dobre wykorzystanie lotności. Cechą charakterystyczną takich flag stanu jest to, że zwykle występuje tylko jedna zmiana stanu; flaga ShutdownRequested zmienia się z false na true, a następnie program zostaje zamknięty. Ten wzorzec można rozszerzyć na flagi stanu, które mogą zmieniać się tam i z powrotem, ale tylko wtedy, gdy cykl przejścia (od fałszu do prawdy do fałszu) nastąpi bez interwencji zewnętrznej. W przeciwnym razie potrzebny jest jakiś atomowy mechanizm przejścia, taki jak zmienne atomowe.
Wzór nr 2: Jednorazowe bezpieczne publikowanie
Błędy widoczności, które mogą wystąpić w przypadku braku synchronizacji, mogą stać się jeszcze trudniejszym problemem podczas zapisywania odniesień do obiektów zamiast pierwotnych wartości. Bez synchronizacji możesz zobaczyć aktualną wartość odniesienia do obiektu zapisanego przez inny wątek i nadal widzieć nieaktualne wartości stanu tego obiektu. (To zagrożenie leży u podstaw problemu związanego z niesławną blokadą podwójnego sprawdzania, w której odniesienie do obiektu jest odczytywane bez synchronizacji, co stwarza ryzyko zobaczenia rzeczywistego odniesienia, ale uzyskania przez nie częściowo skonstruowanego obiektu.) Jednym ze sposobów bezpiecznej publikacji object polega na utworzeniu odniesienia do obiektu lotnego. Listing 3 pokazuje przykład, w którym podczas uruchamiania wątek działający w tle ładuje część danych z bazy danych. Inny kod, który może próbować użyć tych danych, przed próbą użycia sprawdza, czy zostały one opublikowane. public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } Jeśli odniesienie do Flooble nie byłoby niestabilne, w kodzie doWork() istniałoby ryzyko zobaczenia częściowo skonstruowanego Flooble podczas próby odniesienia się do Flooble. Kluczowym wymaganiem dla tego wzorca jest to, że opublikowany obiekt musi być bezpieczny dla wątków lub skutecznie niezmienny (efektywnie niezmienny oznacza, że ​​jego stan nigdy się nie zmienia po opublikowaniu). Łącze Volatile może zapewnić, że obiekt będzie widoczny w opublikowanej formie, ale jeśli stan obiektu zmieni się po opublikowaniu, wymagana jest dodatkowa synchronizacja.
Wzór nr 3: Niezależne obserwacje
Innym prostym przykładem bezpiecznego użycia volatile jest okresowe „publikowanie” obserwacji w celu wykorzystania ich w programie. Na przykład istnieje czujnik środowiskowy, który wykrywa aktualną temperaturę. Wątek tła może co kilka sekund odczytywać ten czujnik i aktualizować zmienną lotną zawierającą aktualną temperaturę. Inne wątki mogą następnie odczytać tę zmienną, wiedząc, że zawarta w niej wartość jest zawsze aktualna. Innym zastosowaniem tego wzorca jest zbieranie statystyk dotyczących programu. Listing 4 pokazuje, jak mechanizm uwierzytelniania może zapamiętać nazwę ostatniego zalogowanego użytkownika. Odniesienie lastUser zostanie ponownie użyte do opublikowania wartości do wykorzystania przez resztę programu. public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } } Ten wzór jest rozwinięciem poprzedniego; wartość jest publikowana do wykorzystania w innym miejscu programu, ale publikacja nie jest jednorazowym zdarzeniem, ale serią niezależnych zdarzeń. Ten wzorzec wymaga, aby opublikowana wartość była faktycznie niezmienna — aby jej stan nie zmienił się po opublikowaniu. Kod korzystający z tej wartości musi mieć świadomość, że w każdej chwili może ona ulec zmianie.
Wzór nr 4: wzór „lotnej fasoli”.
Wzorzec „volatile bean” ma zastosowanie w frameworkach, które używają JavaBeans jako „uwielbionych struktur”. Wzorzec „volatile bean” wykorzystuje komponent JavaBean jako kontener dla grupy niezależnych właściwości z modułami pobierającymi i/lub ustawiającymi. Uzasadnieniem wzorca „volatile bean” jest to, że wiele frameworków udostępnia kontenery dla modyfikowalnych posiadaczy danych (takich jak HttpSession), ale obiekty umieszczone w tych kontenerach muszą być bezpieczne dla wątków. We wzorcu volatile bean wszystkie elementy danych JavaBean są niestabilne, a programy pobierające i ustawiające powinny być proste — nie powinny zawierać żadnej logiki innej niż pobieranie lub ustawianie odpowiedniej właściwości. Ponadto w przypadku elementów danych będących odniesieniami do obiektów wspomniane obiekty muszą być faktycznie niezmienne. (To uniemożliwia pola odwołań do tablicy, ponieważ gdy odwołanie do tablicy jest zadeklarowane jako lotne, tylko to odwołanie, a nie same elementy, ma właściwość volatile.) Podobnie jak w przypadku każdej zmiennej lotnej, nie mogą istnieć żadne niezmienniki ani ograniczenia powiązane z właściwościami JavaBeans . Przykład komponentu JavaBean napisanego przy użyciu wzorca „volatile bean” pokazano na Listingu 5: @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
Bardziej złożone wzory lotne
Wzorce z poprzedniej sekcji obejmują większość typowych przypadków, w których użycie volatile jest rozsądne i oczywiste. W tej sekcji omówiono bardziej złożony wzorzec, w którym volatile może zapewnić poprawę wydajności lub skalowalności. Bardziej zaawansowane, zmienne wzorce mogą być niezwykle delikatne. Bardzo ważne jest, aby Twoje założenia były dokładnie udokumentowane i aby te wzorce były mocno hermetyzowane, ponieważ nawet najmniejsze zmiany mogą złamać Twój kod! Ponadto, biorąc pod uwagę, że głównym powodem bardziej złożonych, niestabilnych przypadków użycia jest wydajność, przed ich użyciem upewnij się, że rzeczywiście odczuwasz wyraźną potrzebę uzyskania zamierzonego wzrostu wydajności. Te wzorce są kompromisami, które poświęcają czytelność lub łatwość konserwacji na rzecz możliwego wzrostu wydajności - jeśli nie potrzebujesz poprawy wydajności (lub nie możesz udowodnić, że jej potrzebujesz za pomocą rygorystycznego programu pomiarowego), to prawdopodobnie jest to zły interes, ponieważ to rezygnujesz z czegoś cennego i otrzymujesz w zamian coś mniej.
Wzór nr 5: Tania blokada odczytu i zapisu
Do tej pory powinieneś być świadomy, że lotność jest zbyt słaba, aby wdrożyć licznik. Ponieważ ++x jest zasadniczo redukcją trzech operacji (odczyt, dołączenie, zapisanie), jeśli coś pójdzie nie tak, utracisz zaktualizowaną wartość, jeśli wiele wątków będzie próbowało jednocześnie zwiększyć licznik ulotny. Jeśli jednak odczytów jest znacznie więcej niż zmian, można połączyć wewnętrzne blokowanie i zmienne niestabilne, aby zmniejszyć ogólny narzut związany ze ścieżką kodu. Listing 6 przedstawia bezpieczny dla wątków licznik, który wykorzystuje synchronized, aby zapewnić, że operacja inkrementacji jest niepodzielna, i używa volatile, aby zapewnić widoczność bieżącego wyniku. Jeśli aktualizacje są rzadkie, to podejście może poprawić wydajność, ponieważ koszty odczytu ograniczają się do odczytów niestabilnych, które są zazwyczaj tańsze niż zakup blokady nie powodującej konfliktów. @ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } } Powodem, dla którego ta metoda nazywa się „tanią blokadą odczytu i zapisu”, jest to, że używasz różnych mechanizmów synchronizacji dla odczytu i zapisu. Ponieważ operacje zapisu w tym przypadku naruszają pierwszy warunek użycia volatile, nie można użyć volatile do bezpiecznego zaimplementowania licznika - należy użyć blokady. Możesz jednak użyć volatile, aby bieżąca wartość była widoczna podczas odczytu, więc użyj blokady dla wszystkich operacji modyfikacji i volatile dla operacji tylko do odczytu. Jeśli blokada pozwala tylko jednemu wątkowi na dostęp do wartości w danym momencie, odczyty lotne pozwalają na więcej niż jeden, więc gdy użyjesz blokady volatile do ochrony odczytu, uzyskasz wyższy poziom wymiany niż w przypadku użycia blokady na całym kodzie: i czyta i nagrywa. Należy jednak pamiętać o kruchości tego wzorca: w przypadku dwóch konkurujących ze sobą mechanizmów synchronizacji może on stać się bardzo skomplikowany, jeśli wyjdzie się poza najbardziej podstawowe zastosowanie tego wzorca.
Streszczenie
Zmienne lotne są prostszą, ale słabszą formą synchronizacji niż blokowanie, która w niektórych przypadkach zapewnia lepszą wydajność lub skalowalność niż blokowanie wewnętrzne. Jeśli spełniasz warunki bezpiecznego stosowania volatile – zmienna jest naprawdę niezależna zarówno od innych zmiennych, jak i od swoich poprzednich wartości – możesz czasami uprościć kod, zastępując synchronized przez volatile. Jednak kod korzystający z funkcji volatile jest często bardziej delikatny niż kod korzystający z blokowania. Sugerowane tutaj wzorce obejmują najczęstsze przypadki, w których zmienność jest rozsądną alternatywą dla synchronizacji. Podążając za tymi wzorcami – i uważając, aby nie przekraczać ich własnych granic – możesz bezpiecznie używać volatile w przypadkach, gdy przynoszą one korzyści.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION