Ważne jest, aby zrozumieć, że istnieją dwa aspekty bezpieczeństwa wątków.
- kontrola wykonania oraz
- 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 synchronized
zapobiega 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.
volatile
Z 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 volatile
również zmienia sposób traktowania long
i double
wymaga 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, A
gdy zapisuje w polu lotnym, f
staje się widoczne dla wątku B
podczas 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 volatile
jest 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+write
instrukcji 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.
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
źródło
synchronized
jest 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ść dosynchronized
bloku. Jeśli inne wątki próbują uzyskać dostęp do tej krytycznej sekcji, muszą poczekać, aż obecny właściciel zwolni blokadę.volatile
jest 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 dovolatile
zmiennych, nie jest wymagane blokowanie . Wszystkie wątki mogą jednocześnie uzyskiwać dostęp do zmiennej wartości zmiennej.Dobry przykład zastosowania zmiennej lotnej:
Date
zmiennej.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.Przeczytaj ten artykuł, aby lepiej zrozumieć
volatile
pojęcie.Lawrence Dol wyjaśnił ci
read-write-update query
.Odnośnie twoich innych zapytań
Musisz użyć,
volatile
jeś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.Odpowiedź będzie taka sama jak w pierwszym zapytaniu.
Zapoznaj się z tym artykułem, aby lepiej zrozumieć.
źródło
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
volatile
potrafi 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
x
pierwotnie 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
synchronized
bloku 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ślix
był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.Oznaczenie
x
jakovolatile
nie powodujex++;
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ścix
moż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ędziefalse
.Oznaczenie zmiennej jako
volatile
zasadniczo mówi wszystkim wątkom, aby wykonywały operacje odczytu i zapisu tylko w pamięci głównej.synchronized
każ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:
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
b
true), ponieważb
jest ustawiony na true dopiero pox
ustawieniu 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. Oznaczenieb
jakovolatile
gwarantuje, ż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 jakovolatile
zapewniają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.
źródło
INC
operację montażu , podstawowe operacje procesora są nadal 3-krotnie i wymagają blokady dla bezpieczeństwa wątków. Słuszna uwaga. ChociażINC/DEC
polecenia mogą być atomowo oflagowane w asemblerze i nadal mogą być 1 operacją atomową.