Różnica między zmienną i zsynchronizowaną w Javie

233

Zastanawiam się nad różnicą między deklarowaniem zmiennej jako volatilei zawsze uzyskiwaniem dostępu do zmiennej w synchronized(this)bloku w Javie?

Zgodnie z tym artykułem http://www.javamex.com/tutorials/synchronization_volatile.shtml jest wiele do powiedzenia i istnieje wiele różnic, ale także pewne podobieństwa.

Szczególnie interesuje mnie ta informacja:

...

  • dostęp do zmiennej ulotnej nigdy nie ma możliwości zablokowania: zawsze wykonujemy tylko prosty odczyt lub zapis, więc w przeciwieństwie do bloku synchronicznego nigdy nie utrzymamy żadnej blokady;
  • ponieważ dostęp do zmiennej lotnej nigdy nie blokuje, nie nadaje się do przypadków, w których chcemy czytać-aktualizować-zapisywać jako operację atomową (chyba że jesteśmy przygotowani na „pominięcie aktualizacji”);

Co rozumieją przez read-update-write ? Czy zapis nie jest także aktualizacją, czy oznacza to po prostu, że aktualizacja zależy od odczytu?

Przede wszystkim, kiedy lepiej jest deklarować zmienne volatileniż uzyskiwać do nich dostęp poprzez synchronizedblok? Czy warto stosować volatilezmienne zależne od danych wejściowych? Na przykład istnieje zmienna o nazwie, renderktóra jest odczytywana przez pętlę renderowania i ustawiana przez naciśnięcie klawisza?

Albus dumbledore
źródło

Odpowiedzi:

383

Ważne jest, aby zrozumieć, że istnieją dwa aspekty bezpieczeństwa wątków.

  1. kontrola wykonania oraz
  2. widoczność pamięci

Pierwszy dotyczy kontrolowania, kiedy kod jest wykonywany (w tym kolejności wykonywania instrukcji) i tego, czy może być wykonywany jednocześnie, a drugi - gdy efekty w pamięci tego, co zostało zrobione, są widoczne dla innych wątków. Ponieważ każdy procesor ma kilka poziomów pamięci podręcznej między nim a pamięcią główną, wątki działające na różnych procesorach lub rdzeniach mogą różnie widzieć „pamięć” w danym momencie, ponieważ wątki mogą uzyskiwać i pracować na prywatnych kopiach pamięci głównej.

Użycie synchronizedzapobiega uzyskaniu monitora (lub blokady) dla tego samego obiektu przez dowolny inny wątek , tym samym uniemożliwiając jednoczesne wykonywanie wszystkich bloków kodu chronionych przez synchronizację na tym samym obiekcie . Synchronizacja tworzy również barierę pamięci „dzieje się przed”, powodując ograniczenie widoczności pamięci, tak że wszystko, co zrobiono do momentu, w którym jakiś wątek zwolni blokadę, pojawia się w innym wątku, który następnie nabył tę samą blokadę , zanim nastąpiło jej nabycie. W praktyce, na obecnym sprzęcie, zazwyczaj powoduje to opróżnianie pamięci podręcznej procesora po zakupie monitora i zapisuje w pamięci głównej po zwolnieniu, które są (stosunkowo) drogie.

volatileZ drugiej strony użycie zmusza wszystkie wejścia (odczyt lub zapis) do zmiennej zmiennej do wystąpienia w głównej pamięci, skutecznie utrzymując zmienną zmienną poza pamięcią podręczną procesora. Może to być przydatne w przypadku niektórych działań, w których po prostu wymagana jest poprawność widoczności zmiennej, a kolejność dostępu nie jest ważna. Zastosowanie volatilerównież zmienia sposób traktowania longi doublewymaga od nich dostępu atomowego; na niektórych (starszych) urządzeniach może to wymagać blokad, choć nie na nowoczesnym 64-bitowym sprzęcie. Zgodnie z nowym (JSR-133) modelem pamięci dla Java 5+, semantyka lotności została wzmocniona, aby była prawie tak silna, jak zsynchronizowana pod względem widoczności pamięci i kolejności instrukcji (patrz http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile). Dla celów widoczności każdy dostęp do pola niestabilnego działa jak połowa synchronizacji.

W nowym modelu pamięci nadal jest prawdą, że zmienne zmienne nie mogą być ze sobą porządkowane. Różnica polega na tym, że nie jest już tak łatwo zmienić kolejność normalnego dostępu do pola wokół nich. Zapis w polu lotnym ma taki sam efekt pamięci, jak zwolnienie monitora, a odczyt z pola niestabilnego ma taki sam efekt pamięci, jak w przypadku monitora. W efekcie, ponieważ nowy model pamięci nakłada surowsze ograniczenia na zmianę kolejności dostępu do pól lotnych z innymi dostępami do pól, zmiennymi lub nie, wszystko, co było widoczne dla wątku, Agdy zapisuje w polu lotnym, fstaje się widoczne dla wątku Bpodczas odczytu f.

- JSR 133 (Java Memory Model) FAQ

Tak więc teraz obie formy bariery pamięci (w ramach obecnego JMM) powodują barierę ponownego zamawiania instrukcji, która uniemożliwia kompilatorowi lub czasowi wykonywania ponownego zamówienia instrukcji przez barierę. W starym JMM zmienność nie zapobiegała ponownemu składaniu zamówień. Może to być ważne, ponieważ oprócz barier pamięciowych jedynym ograniczeniem jest to, że dla każdego konkretnego wątku efekt netto kodu jest taki sam, jak gdyby instrukcje były wykonywane dokładnie w kolejności, w jakiej występują w źródło.

Jednym zastosowaniem lotnego jest współdzielony, ale niezmienny obiekt jest odtwarzany w locie, przy czym wiele innych wątków odwołuje się do obiektu w określonym punkcie ich cyklu wykonywania. Potrzebne są inne wątki, aby rozpocząć korzystanie z odtworzonego obiektu po jego opublikowaniu, ale nie potrzeba dodatkowego obciążenia wynikającego z pełnej synchronizacji i towarzyszącego mu sporu i opróżnienia pamięci podręcznej.

// Declaration
public class SharedLocation {
    static public SomeObject someObject=new SomeObject(); // default object
    }

// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
//       someObject will be internally consistent for xxx(), a subsequent 
//       call to yyy() might be inconsistent with xxx() if the object was 
//       replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using code
private String getError() {
    SomeObject myCopy=SharedLocation.someObject; // gets current copy
    ...
    int cod=myCopy.getErrorCode();
    String txt=myCopy.getErrorText();
    return (cod+" - "+txt);
    }
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.

Mówiąc konkretnie na twoje pytanie odczytu-aktualizacji-zapisu. Rozważ następujący niebezpieczny kod:

public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }

Teraz, gdy metoda updateCounter () nie jest zsynchronizowana, dwa wątki mogą do niej wejść jednocześnie. Jedną z wielu kombinacji tego, co może się zdarzyć, jest to, że wątek 1 sprawdza licznik == 1000 i stwierdza, że ​​jest to prawda, a następnie zostaje zawieszony. Następnie wątek 2 wykonuje ten sam test, a także sprawdza, czy jest prawdziwy i jest zawieszony. Następnie wątek-1 wznawia się i ustawia licznik na 0. Następnie wątek-2 wznawia się i ponownie ustawia licznik na 0, ponieważ brakowało aktualizacji z wątku-1. Może się to również zdarzyć, nawet jeśli przełączanie wątków nie nastąpi tak, jak to opisałem, ale po prostu dlatego, że dwie różne buforowane kopie licznika były obecne w dwóch różnych rdzeniach procesora, a wątki działały na osobnym rdzeniu. Jeśli o to chodzi, jeden wątek może mieć licznik na jednej wartości, a drugi może mieć licznik na zupełnie innej wartości tylko z powodu buforowania.

W tym przykładzie ważne jest to, że licznik zmiennych został odczytany z pamięci głównej do pamięci podręcznej, zaktualizowany w pamięci podręcznej i zapisany z powrotem do pamięci głównej tylko w pewnym nieokreślonym punkcie później, kiedy pojawiła się bariera pamięci lub gdy pamięć podręczna była potrzebna na coś innego. Wykonanie licznika volatilejest niewystarczające dla bezpieczeństwa wątków tego kodu, ponieważ test dla maksimum i przypisania są operacjami dyskretnymi, w tym przyrostem, który jest zestawem nieatomowych read+increment+writeinstrukcji maszynowych, na przykład:

MOV EAX,counter
INC EAX
MOV counter,EAX

Zmienne zmienne są użyteczne tylko wtedy, gdy wszystkie wykonywane na nich operacje mają charakter „atomowy”, na przykład w moim przykładzie, w którym odwołanie do w pełni ukształtowanego obiektu jest odczytywane lub zapisywane (i faktycznie zazwyczaj jest zapisywane tylko z jednego punktu). Innym przykładem może być ulotna referencja tablicowa, na której opiera się lista kopiowania przy zapisie, pod warunkiem, że tablica została odczytana tylko przez pobranie lokalnej kopii referencji do niej.

Lawrence Dol
źródło
5
Dziękuję bardzo! Przykład z licznikiem jest prosty do zrozumienia. Jednak gdy rzeczy stają się rzeczywistością, jest trochę inaczej.
Albus Dumbledore,
„W praktyce na obecnym sprzęcie zwykle powoduje to opróżnianie pamięci podręcznej procesora po zakupie monitora i zapisuje w pamięci głównej po zwolnieniu, które są drogie (względnie mówiąc)”. . Kiedy mówisz, że pamięć podręczna procesora jest taka sama, jak stosy Java lokalne dla każdego wątku? lub czy wątek ma własną lokalną wersję Sterty? Przepraszam, jeśli jestem tu głupi.
NishM
1
@nishm To nie jest to samo, ale obejmowałoby lokalne bufory zaangażowanych wątków. .
Lawrence Dol
1
@ MarianPaździoch: Przyrost lub spadek NIE jest czytaniem ani pisaniem, jest czytaniem i pisaniem; jest to odczyt do rejestru, następnie przyrost rejestru, a następnie zapis z powrotem do pamięci. Odczyty i zapisy są pojedynczo atomowe, ale wiele takich operacji nie.
Lawrence Dol
2
Tak więc, zgodnie z często zadawanymi pytaniami, nie tylko akcje wykonane od momentu uzyskania blokady są widoczne po odblokowaniu, ale wszystkie akcje wykonane przez ten wątek są widoczne. Nawet działania wykonane przed nabyciem blokady.
Lii,
97

volatile to modyfikator pola , a synchronizacja modyfikuje bloki kodu i metody . Możemy więc określić trzy warianty prostego akcesorium za pomocą tych dwóch słów kluczowych:

    int i1;
    int geti1() {return i1;}

    volatile int i2;
    int geti2() {return i2;}

    int i3;
    synchronized int geti3() {return i3;}

geti1()uzyskuje dostęp do wartości aktualnie zapisanej w i1bieżącym wątku. Wątki mogą mieć lokalne kopie zmiennych, a dane nie muszą być takie same jak dane przechowywane w innych wątkach. W szczególności inny wątek mógł zostać zaktualizowany i1w swoim wątku, ale wartość w bieżącym wątku może być inna niż ta zaktualizowana wartość. W rzeczywistości Java ma ideę „głównej” pamięci, a ta pamięć zawiera aktualną „poprawną” wartość zmiennych. Wątki mogą mieć własną kopię danych dla zmiennych, a kopia wątku może różnić się od „głównej” pamięci. Tak więc w rzeczywistości możliwe jest, że pamięć „główna” ma wartość 1 dla i1, dla wątku 1 wartość 2 dla i1i dla wątku 2mieć wartość 3 dla i1jeśli thread1 i thread2 mieć zarówno zaktualizowane i1 ale te zaktualizowana wartość nie została jeszcze propagowane do „głównego” pamięci lub innych wątków.

Z drugiej strony geti2()skutecznie uzyskuje dostęp do wartości i2z „głównej” pamięci. Zmienna lotna nie może mieć lokalnej kopii zmiennej innej niż wartość przechowywana obecnie w „głównej” pamięci. W efekcie zmienna zadeklarowana jako niestabilna musi mieć zsynchronizowane dane we wszystkich wątkach, aby za każdym razem, gdy uzyskiwałeś dostęp do zmiennej w dowolnym wątku lub ją aktualizowałeś, wszystkie pozostałe wątki natychmiast widziały tę samą wartość. Zasadniczo zmienne zmienne mają większy dostęp i aktualizują koszty ogólne niż zmienne „zwykłe”. Zasadniczo wątki mogą mieć własne kopie danych, co zapewnia lepszą wydajność.

Istnieją dwie różnice między volitile a synchronizowanym.

Najpierw zsynchronizowane uzyskuje i zwalnia blokady na monitorach, które mogą zmusić tylko jeden wątek na raz do wykonania bloku kodu. To dość dobrze znany aspekt synchronizacji. Ale synchronizacja synchronizuje również pamięć. W rzeczywistości synchronizacja synchronizuje całą pamięć wątków z pamięcią „główną”. Wykonanie geti3()wykonuje następujące czynności:

  1. Wątek uzyskuje blokadę na monitorze dla obiektu tego.
  2. Pamięć wątków opróżnia wszystkie zmienne, tzn. Skutecznie odczytuje wszystkie zmienne z pamięci „głównej”.
  3. Blok kodu jest wykonywany (w tym przypadku ustawienie wartości zwracanej na bieżącą wartość i3, która mogła właśnie zostać zresetowana z „głównej” pamięci).
  4. (Wszelkie zmiany zmiennych normalnie byłyby teraz zapisywane w „głównej” pamięci, ale dla geti3 () nie mamy żadnych zmian).
  5. Wątek zwalnia blokadę na monitorze dla obiektu tego.

Tak więc, gdzie volatile synchronizuje tylko wartość jednej zmiennej między pamięcią wątku a pamięcią „główną”, synchronizuje synchronizuje wartość wszystkich zmiennych między pamięcią wątku a pamięcią „główną” oraz blokuje i zwalnia monitor w celu uruchomienia. Wyraźnie zsynchronizowany prawdopodobnie będzie miał większy narzut niż lotny.

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html

Kerem Baydoğan
źródło
35
-1, Volatile nie uzyskuje blokady, używa podstawowej architektury procesora, aby zapewnić widoczność we wszystkich wątkach po zapisie.
Michael Barker,
Warto zauważyć, że mogą istnieć przypadki, w których można użyć blokady, aby zagwarantować atomowość zapisów. Np. Pisanie długiej na 32-bitowej platformie, która nie obsługuje praw do rozszerzonej szerokości. Intel unika tego, wykorzystując rejestry SSE2 (szerokość 128 bitów) do obsługi niestabilnych długów. Jednak rozważanie niestabilności jako blokady prawdopodobnie doprowadzi do nieprzyjemnych błędów w kodzie.
Michael Barker
2
Ważnym semantycznym wspólnym dla blokowania zmiennych niestabilnych jest to, że oba zapewniają krawędzie Happens-Before (Java 1.5 i nowsze). Wchodzenie do zsynchronizowanego bloku, wyjmowanie blokady i czytanie z substancji lotnej są uważane za „uzyskanie”, a zwolnienie blokady, wyjście z synchronizowanego bloku i napisanie substancji lotnej są formami „uwolnienia”.
Michael Barker
20

synchronizedjest modyfikatorem ograniczenia dostępu na poziomie metody / bloku. Zapewni to, że jeden wątek posiada zamek do sekcji krytycznej. Tylko wątek posiadający zamek może wejść do synchronizedbloku. Jeśli inne wątki próbują uzyskać dostęp do tej krytycznej sekcji, muszą poczekać, aż obecny właściciel zwolni blokadę.

volatilejest modyfikatorem dostępu do zmiennych, który zmusza wszystkie wątki do pobierania najnowszej wartości zmiennej z pamięci głównej. Aby uzyskać dostęp do volatilezmiennych, nie jest wymagane blokowanie . Wszystkie wątki mogą jednocześnie uzyskiwać dostęp do zmiennej wartości zmiennej.

Dobry przykład zastosowania zmiennej lotnej: Datezmiennej.

Załóżmy, że zmieniono datę volatile. Wszystkie wątki, które uzyskują dostęp do tej zmiennej, zawsze otrzymują najnowsze dane z pamięci głównej, dzięki czemu wszystkie wątki wyświetlają rzeczywistą (aktualną) wartość Data. Nie potrzebujesz różnych wątków pokazujących inny czas dla tej samej zmiennej. Wszystkie wątki powinny pokazywać właściwą wartość daty.

wprowadź opis zdjęcia tutaj

Przeczytaj ten artykuł, aby lepiej zrozumieć volatilepojęcie.

Lawrence Dol wyjaśnił ci read-write-update query.

Odnośnie twoich innych zapytań

Kiedy lepiej jest zadeklarować zmienne zmienne niż uzyskać do nich dostęp poprzez synchronizację?

Musisz użyć, volatilejeśli uważasz, że wszystkie wątki powinny uzyskać rzeczywistą wartość zmiennej w czasie rzeczywistym, jak w przykładzie, który wyjaśniłem dla zmiennej Date.

Czy warto stosować zmienny w przypadku zmiennych zależnych od danych wejściowych?

Odpowiedź będzie taka sama jak w pierwszym zapytaniu.

Zapoznaj się z tym artykułem, aby lepiej zrozumieć.

Ravindra babu
źródło
Czytanie może odbywać się w tym samym czasie, a wszystkie wątki będą czytać najnowszą wartość, ponieważ procesor nie buforuje pamięci głównej do pamięci podręcznej wątków procesora, ale co z zapisem? Zapis nie może być równoległy, poprawny? Drugie pytanie: jeśli blok jest zsynchronizowany, ale zmienna nie jest ulotna, wartość zmiennej w zsynchronizowanym bloku można nadal zmienić za pomocą innego wątku w innym bloku kodu, prawda?
the_prole
11

tl; dr :

Istnieją 3 główne problemy z wielowątkowością:

1) Warunki wyścigu

2) Pamięć podręczna / nieaktualna pamięć

3) Optymalizacja kompilatora i procesora

volatilepotrafi rozwiązać 2 i 3, ale nie może rozwiązać 1. synchronized/ jawne blokady mogą rozwiązać 1, 2 i 3.

Opracowanie :

1) Rozważ ten niebezpieczny kod wątku:

x++;

Chociaż może to wyglądać jak jedna operacja, w rzeczywistości jest to 3: odczyt bieżącej wartości x z pamięci, dodanie 1 do niej i zapisanie jej z powrotem w pamięci. Jeśli kilka wątków próbuje to zrobić jednocześnie, wynik operacji jest niezdefiniowany. Jeśli xpierwotnie był to 1, po 2 wątkach obsługujących kod może to być 2 i może być 3, w zależności od tego, który wątek zakończył, która część operacji przed kontrolą została przeniesiona do drugiego wątku. To forma wyścigu .

Użycie synchronizedbloku kodu powoduje, że staje się on atomowy - co oznacza, że ​​sprawia, że ​​są to 3 operacje naraz, i nie ma sposobu, aby inny wątek wszedł w środek i przeszkodził. Więc jeśli xbyło 1, a 2 wątki próbują wykonać formę wstępną x++, wiemy , że w końcu będzie równa 3. Więc to rozwiązuje problem warunków wyścigu.

synchronized (this) {
   x++; // no problem now
}

Oznaczenie xjako volatilenie powoduje x++;atomizacji, więc nie rozwiązuje tego problemu.

2) Ponadto wątki mają swój własny kontekst - tzn. Mogą buforować wartości z pamięci głównej. Oznacza to, że kilka wątków może zawierać kopie zmiennej, ale działają one na kopii roboczej bez współdzielenia nowego stanu zmiennej między innymi wątkami.

Uważają, że na jednej nici x = 10;. I nieco później, w innym wątku, x = 20;. Zmiana wartości xmoże nie pojawić się w pierwszym wątku, ponieważ drugi wątek zapisał nową wartość w swojej pamięci roboczej, ale nie skopiował jej do pamięci głównej. Lub że skopiował go do pamięci głównej, ale pierwszy wątek nie zaktualizował kopii roboczej. Więc jeśli teraz pierwszy wątek sprawdzi, if (x == 20)odpowiedź będzie false.

Oznaczenie zmiennej jako volatilezasadniczo mówi wszystkim wątkom, aby wykonywały operacje odczytu i zapisu tylko w pamięci głównej. synchronizedkaże każdemu wątkowi przejść aktualizację wartości z pamięci głównej po wejściu do bloku i opróżnić wynik z powrotem do pamięci głównej po wyjściu z bloku.

Zauważ, że w przeciwieństwie do wyścigów danych, przestarzała pamięć nie jest tak łatwa do (ponownego) wytworzenia, ponieważ i tak następuje opróżnienie pamięci głównej.

3) Kompilator i procesor mogą (bez żadnej synchronizacji między wątkami) traktować cały kod jako jednowątkowy. Oznacza to, że może patrzeć na jakiś kod, który jest bardzo znaczący w aspekcie wielowątkowości i traktować go tak, jakby był jednowątkowy, gdzie nie jest tak znaczący. Może więc spojrzeć na kod i, w celu optymalizacji, zdecydować o jego ponownym uporządkowaniu lub nawet całkowitym usunięciu jego części, jeśli nie wie, że ten kod działa z wieloma wątkami.

Rozważ następujący kod:

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

Można by pomyśleć, że wątek B może wydrukować tylko 20 (lub w ogóle nic nie wydrukować, jeśli sprawdzanie wątku B zostanie wykonane przed ustawieniem wartości btrue), ponieważ bjest ustawiony na true dopiero po xustawieniu wartości 20, ale kompilator / procesor może zdecydować o zmianie kolejności Wątek A, w tym przypadku wątek B może również wydrukować 10. Oznaczenie bjako volatilegwarantuje, że nie zostanie zmieniony (lub w niektórych przypadkach odrzucony). Co oznacza, że ​​wątek B może wydrukować tylko 20 (lub w ogóle nic). Oznaczenie metod jako zsynchronizowanych pozwoli osiągnąć ten sam wynik. Oznaczenie zmiennej jako volatilezapewniającej tylko, że nie zostanie ona ponownie uporządkowana, ale wszystko przed / po niej może być nadal uporządkowane, więc synchronizacja może być bardziej odpowiednia w niektórych scenariuszach.

Pamiętaj, że przed nowym modelem pamięci Java 5 lotne nie rozwiązało tego problemu.

David Refaeli
źródło
1
„Chociaż może to wyglądać jak jedna operacja, w rzeczywistości jest to 3: odczyt bieżącej wartości x z pamięci, dodanie 1 do niej i zapisanie jej z powrotem w pamięci.” - Racja, ponieważ wartości z pamięci muszą przejść przez obwody procesora, aby mogły zostać dodane / zmodyfikowane. Mimo że przekształca się to tylko w jedną INCoperację montażu , podstawowe operacje procesora są nadal 3-krotnie i wymagają blokady dla bezpieczeństwa wątków. Słuszna uwaga. Chociaż INC/DECpolecenia mogą być atomowo oflagowane w asemblerze i nadal mogą być 1 operacją atomową.
Zombie
@Zombies, więc kiedy tworzę zsynchronizowany blok dla x ++, czy zamienia go on w oznaczony atomowy INC / DEC, czy używa zwykłej blokady?
David Refaeli
Nie wiem! Wiem tylko, że INC / DEC nie są atomowe, ponieważ dla procesora musi on załadować wartość i PRZECZYTAĆ ją, a także ZAPISAĆ (do pamięci), jak każda inna operacja arytmetyczna.
Zombie